mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-07 01:19:17 +00:00
Merge branch 'develop' into fix/1270-verification-request-timeout
This commit is contained in:
@@ -7,7 +7,6 @@
|
|||||||
"PR-Dependencies",
|
"PR-Dependencies",
|
||||||
],
|
],
|
||||||
"ignoreDeps": [
|
"ignoreDeps": [
|
||||||
"string:app_name",
|
|
||||||
"gradle",
|
"gradle",
|
||||||
],
|
],
|
||||||
"minimumReleaseAge": "7 days",
|
"minimumReleaseAge": "7 days",
|
||||||
|
|||||||
@@ -31,9 +31,6 @@ __pycache__/
|
|||||||
# Local configuration file (sdk path, etc)
|
# Local configuration file (sdk path, etc)
|
||||||
local.properties
|
local.properties
|
||||||
|
|
||||||
# Proguard folder generated by Eclipse
|
|
||||||
proguard/
|
|
||||||
|
|
||||||
# Log Files
|
# Log Files
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
<component name="CopyrightManager">
|
<component name="CopyrightManager">
|
||||||
<copyright>
|
<copyright>
|
||||||
<option name="notice" value="© &#36;today.year Element Creations Ltd. Element Creations Ltd, Element Software SARL, Element Software Inc., and Element Software GmbH (the "Element Group") only make this file available under a proprietary license model. Without a proprietary license with us, you cannot use this file. The terms of the proprietary license agreement between you and any member of the Element Group shall always apply to your use of this file. Unauthorised use, copying, distribution, or modification of this file, via any medium, is strictly prohibited. For details about the licensing terms, you must either visit our website or contact a member of our sales team." />
|
<option name="notice" value="© &#36;today.year Element Creations Ltd. SPDX-License-Identifier: LicenseRef-Element-Commercial SPDX-License-Identifier: LicenseRef-Element-Proprietary" />
|
||||||
<option name="myName" value="Element Enterprise" />
|
<option name="myName" value="Element Enterprise" />
|
||||||
</copyright>
|
</copyright>
|
||||||
</component>
|
</component>
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
appId: ${MAESTRO_APP_ID}
|
|
||||||
---
|
|
||||||
- tapOn:
|
|
||||||
id: "login-change_server"
|
|
||||||
- takeScreenshot: build/maestro/200-ChangeServer
|
|
||||||
- tapOn: "matrix.org"
|
|
||||||
- tapOn:
|
|
||||||
id: "login-change_server"
|
|
||||||
- tapOn: "Other"
|
|
||||||
- tapOn:
|
|
||||||
id: "change_server-server"
|
|
||||||
- inputText: "element"
|
|
||||||
- hideKeyboard
|
|
||||||
- extendedWaitUntil:
|
|
||||||
visible: "element.io"
|
|
||||||
timeout: 10000
|
|
||||||
- tapOn: "element.io"
|
|
||||||
# Revert to matrix.org
|
|
||||||
- tapOn:
|
|
||||||
id: "login-change_server"
|
|
||||||
- tapOn: "matrix.org"
|
|
||||||
@@ -3,8 +3,7 @@ appId: ${MAESTRO_APP_ID}
|
|||||||
- tapOn: "Sign in manually"
|
- tapOn: "Sign in manually"
|
||||||
- runFlow: ../assertions/assertLoginDisplayed.yaml
|
- runFlow: ../assertions/assertLoginDisplayed.yaml
|
||||||
- takeScreenshot: build/maestro/100-SignIn
|
- takeScreenshot: build/maestro/100-SignIn
|
||||||
- runFlow: changeServer.yaml
|
# The account provider field defaults to matrix.org, so continue straight to the MAS login.
|
||||||
- runFlow: ../assertions/assertLoginDisplayed.yaml
|
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: "login-continue"
|
id: "login-continue"
|
||||||
## MAS page
|
## MAS page
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
appId: ${MAESTRO_APP_ID}
|
appId: ${MAESTRO_APP_ID}
|
||||||
---
|
---
|
||||||
- extendedWaitUntil:
|
- extendedWaitUntil:
|
||||||
visible: "Change account provider"
|
visible: "Enter your account provider"
|
||||||
timeout: 10000
|
timeout: 10000
|
||||||
|
|||||||
@@ -37,8 +37,9 @@ PRs must meet these rules.
|
|||||||
|
|
||||||
- Create previews for **all main states** of a Composable.
|
- Create previews for **all main states** of a Composable.
|
||||||
- Use `@PreviewsDayNight` for consistency.
|
- Use `@PreviewsDayNight` for consistency.
|
||||||
- Use `PreviewParameterProvider` (e.g., `FooStateProvider`) to provide states.
|
- Use `PreviewParameterProvider` (e.g., `FooStatePreviewParam`) to provide states.
|
||||||
- Wrap previews in `ElementPreview { ... }`.
|
- Wrap previews in `ElementPreview { ... }`.
|
||||||
|
- When writing tests, never try to record the screenshots, the CI will do it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ We use [Appyx](https://bumble-tech.github.io/appyx/) for navigation and [Molecul
|
|||||||
| `FooView.kt` | Stateless Composable rendering the UI from `FooState`. |
|
| `FooView.kt` | Stateless Composable rendering the UI from `FooState`. |
|
||||||
| `FooState.kt` | Data class representing the immutable UI state. |
|
| `FooState.kt` | Data class representing the immutable UI state. |
|
||||||
| `FooEvent.kt` | Sealed interface for UI actions sent to the Presenter. |
|
| `FooEvent.kt` | Sealed interface for UI actions sent to the Presenter. |
|
||||||
| `FooStateProvider.kt` | Provides sample states for Previews and Screenshot tests. |
|
| `FooStatePreviewParam.kt` | Provides sample states for Previews and Screenshot tests. |
|
||||||
| `FooPresenterTest.kt` | Unit tests for the Presenter logic using Turbine. |
|
| `FooPresenterTest.kt` | Unit tests for the Presenter logic using Turbine. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+83
@@ -1,3 +1,86 @@
|
|||||||
|
Changes in Element X v26.08.2
|
||||||
|
=============================
|
||||||
|
|
||||||
|
<!-- Release notes generated using configuration in .github/release.yml at v26.08.2 -->
|
||||||
|
|
||||||
|
**Note: this release in an internal release, it will not be pushed to the stores.**
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
### ✨ Features
|
||||||
|
* Remove the legacy web-based account creation for matrix.org by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7412
|
||||||
|
### 🙌 Improvements
|
||||||
|
* Move the custom tile server URL config to the Element wellknown content by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7416
|
||||||
|
* Explain a rate limiting push gateway in the notification troubleshooter by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7448
|
||||||
|
* Move the `:libraries:wellknown` logic into `enterprise` modules by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7429
|
||||||
|
* Refresh the room members when a mention is started by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7438
|
||||||
|
* Close the invite screen once the invite has been declined by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7440
|
||||||
|
* Use Profiles sliding sync extension check for own profile subscription by @ganfra in https://github.com/element-hq/element-x-android/pull/7403
|
||||||
|
* Simplify choosing the account provider during sign in, with autocomplete of previously used ones by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7394
|
||||||
|
### 🐛 Bugfixes
|
||||||
|
* Fix wrong TextComposer placeholder displayed when in a thread. by @bmarty in https://github.com/element-hq/element-x-android/pull/7413
|
||||||
|
* Fix bug reports failing to send after the app has crashed by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7395
|
||||||
|
* Fix outgoing session verification stuck with incomplete recovery setup by @hardcore-sushi in https://github.com/element-hq/element-x-android/pull/7433
|
||||||
|
* Keep transparency when an image is shared into the app with an imprecise mime type by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7452
|
||||||
|
* Don't restart the sync service if the client is shutting down by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7472
|
||||||
|
* Normalize the applicationName used in user agent by @bmarty in https://github.com/element-hq/element-x-android/pull/7477
|
||||||
|
* Keep playing a voice message when another app plays a short sound by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7454
|
||||||
|
* Keep the send button when the composer is emptied while editing a message by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7456
|
||||||
|
* Do not open the room again when the app is resumed from the recents list by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7436
|
||||||
|
* Stop waiting for a room alias that the remote server never resolves by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7446
|
||||||
|
### 🧱 Build
|
||||||
|
* build-rust-sdk: print the command we run by @richvdh in https://github.com/element-hq/element-x-android/pull/7407
|
||||||
|
* Remove license check on Pro modules. by @bmarty in https://github.com/element-hq/element-x-android/pull/7418
|
||||||
|
* Improve build time config and update the Firebase configuration by @bmarty in https://github.com/element-hq/element-x-android/pull/7473
|
||||||
|
* Make the PUSHER_APP_ID configurable at build time. by @bmarty in https://github.com/element-hq/element-x-android/pull/7478
|
||||||
|
### 📄 Documentation
|
||||||
|
* Add KDoc to all the interfaces of the api modules by @bmarty in https://github.com/element-hq/element-x-android/pull/7426
|
||||||
|
### 🚧 In development 🚧
|
||||||
|
* [LinkNewDevice] Ensure the notice screen is also protected by the timeout. by @bmarty in https://github.com/element-hq/element-x-android/pull/7428
|
||||||
|
### Dependency upgrades
|
||||||
|
* Update kotlin to v2.3.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7396
|
||||||
|
* Update plugin sonarqube to v7.4.0.8496 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7415
|
||||||
|
* Update metro to v1.4.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7421
|
||||||
|
* Update roborazzi to v1.71.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7423
|
||||||
|
* Update dependency io.sentry:sentry-android to v8.52.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7414
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7420
|
||||||
|
* Update dependency io.github.sergio-sastre.ComposablePreviewScanner:android to v0.9.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7431
|
||||||
|
* Update dependency org.maplibre.compose:maplibre-compose to v0.14.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7432
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.17 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7460
|
||||||
|
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.19 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7479
|
||||||
|
* Update media3 to v1.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7427
|
||||||
|
* Update dependency androidx.compose:compose-bom to v2026.08.00 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7483
|
||||||
|
### 💼 Element Pro
|
||||||
|
* Add hooks to allow providing additional feature flags in EX pro by @richvdh in https://github.com/element-hq/element-x-android/pull/7419
|
||||||
|
* Use the right base URL for the ESS config/well known endpoints by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7469
|
||||||
|
### Others
|
||||||
|
* Do not display pinned events that the client cannot render by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7379
|
||||||
|
* Hide the forward action on live location shares by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7343
|
||||||
|
* Do not fail the screenshot recording job when there is nothing to commit by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7355
|
||||||
|
* Show the account a UnifiedPush registration belongs to in the distributor by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7345
|
||||||
|
* Stop repeating messages that are already shown in a room notification by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7370
|
||||||
|
* Turn a matrix: URI in a message into a pill by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7354
|
||||||
|
* Send a message with Enter on a physical keyboard, Shift+Enter for a new line by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7353
|
||||||
|
* Do not group timeline messages sent more than five minutes apart by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7349
|
||||||
|
* Linkify URLs whose host is a bracketed IPv6 address by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7347
|
||||||
|
* Add missing tests for `RustTemporaryMatrixClient` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7417
|
||||||
|
* Revert "Keep manually revealed media visible when re-entering a room" by @bmarty in https://github.com/element-hq/element-x-android/pull/7424
|
||||||
|
* Fix outdated documentation. by @bmarty in https://github.com/element-hq/element-x-android/pull/7430
|
||||||
|
* Sync strings - New language supported: Arabic by @bmarty in https://github.com/element-hq/element-x-android/pull/7461
|
||||||
|
* Keep the focused login field visible above the keyboard by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7455
|
||||||
|
* Relax image compression so photos and screenshots keep more detail by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7451
|
||||||
|
* Open the user profile of a user id that contains a slash by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7445
|
||||||
|
* Remove the F-Droid dex patching step from the release script by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7442
|
||||||
|
* Convert markdown to HTML when a caption is edited by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7441
|
||||||
|
* Exclude carriage returns from the login and password fields by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7439
|
||||||
|
* Show an error when the room to leave cannot be found by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7437
|
||||||
|
* Give the room name in the timeline top bar a 48dp tap target by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7450
|
||||||
|
* Model the fake voice recorder on a cancellable recording job by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7466
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @hardcore-sushi made their first contribution in https://github.com/element-hq/element-x-android/pull/7433
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.1...v26.08.2
|
||||||
|
|
||||||
Changes in Element X v26.08.1
|
Changes in Element X v26.08.1
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
|
|||||||
@@ -14,16 +14,16 @@ import kotlin.reflect.KClass
|
|||||||
* Adds Node to the specified component graph.
|
* Adds Node to the specified component graph.
|
||||||
* Equivalent to the following declaration:
|
* Equivalent to the following declaration:
|
||||||
*
|
*
|
||||||
|
* ```
|
||||||
* @BindingContainer
|
* @BindingContainer
|
||||||
* @ContributesTo(Scope::class)
|
* @ContributesTo(Scope::class)
|
||||||
* abstract class YourNodeModule {
|
* interface YourNodeBindingContainer {
|
||||||
|
* @Binds
|
||||||
* @Binds
|
* @IntoMap
|
||||||
* @IntoMap
|
* @NodeKey(YourNode::class)
|
||||||
* @NodeKey(YourNode::class)
|
* fun bindYourNodeAssistedFactory(factory: YourNodeAssistedFactory): AssistedNodeFactory<*>
|
||||||
* abstract fun bindYourNodeFactory(factory: YourNode.Factory): AssistedNodeFactory<*>
|
* }
|
||||||
*}
|
* ```
|
||||||
|
|
||||||
*/
|
*/
|
||||||
@Target(AnnotationTarget.CLASS)
|
@Target(AnnotationTarget.CLASS)
|
||||||
annotation class ContributesNode(
|
annotation class ContributesNode(
|
||||||
|
|||||||
+28
-18
@@ -127,24 +127,11 @@ android {
|
|||||||
optimization {
|
optimization {
|
||||||
enable = true
|
enable = true
|
||||||
keepRules {
|
keepRules {
|
||||||
files.add(File(projectDir, "common-proguard-rules.pro"))
|
// Equivalent of adding `getDefaultProguardFile("proguard-android-optimize.txt")` (this is the default value).
|
||||||
files.add(getDefaultProguardFile("proguard-android-optimize.txt"))
|
includeDefault = true
|
||||||
|
|
||||||
// Depending on whether the app flavor is enterprise or not we want to use different proguard rules.
|
|
||||||
val flavorProguardFile = if (isEnterpriseBuild) {
|
|
||||||
// Custom rules for enterprise builds
|
|
||||||
File(projectDir, "enterprise-proguard-rules.pro")
|
|
||||||
} else {
|
|
||||||
// These default rules prevent the OSS app from being obfuscated
|
|
||||||
File(projectDir, "default-proguard-rules.pro")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flavorProguardFile.exists()) {
|
|
||||||
files.add(flavorProguardFile)
|
|
||||||
} else {
|
|
||||||
logger.warn("Proguard file ${flavorProguardFile.absolutePath} does not exist")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Our custom keep rules are registered as `keepRules` source folders in the `androidComponents` block below,
|
||||||
|
// as the former `keepRules.files` DSL is deprecated since AGP 9.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,6 +216,30 @@ androidComponents {
|
|||||||
)
|
)
|
||||||
|
|
||||||
onVariants { variant ->
|
onVariants { variant ->
|
||||||
|
// Register the R8 keep rules source folders for optimized build types (release, nightly).
|
||||||
|
// Replaces the deprecated `optimization.keepRules.files` DSL (AGP 9+).
|
||||||
|
if (variant.buildType != "debug") {
|
||||||
|
variant.sources.keepRules?.let { keepRules ->
|
||||||
|
// Common rules, always applied.
|
||||||
|
keepRules.addStaticSourceDirectory("proguard/common")
|
||||||
|
|
||||||
|
// Depending on whether the app flavor is enterprise or not we want to use different proguard rules.
|
||||||
|
val flavorProguardDir = if (isEnterpriseBuild) {
|
||||||
|
// Custom rules for enterprise builds
|
||||||
|
"../enterprise/proguard"
|
||||||
|
} else {
|
||||||
|
// Custom fules for FOSS builds
|
||||||
|
"proguard/foss"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File(projectDir, flavorProguardDir).exists()) {
|
||||||
|
keepRules.addStaticSourceDirectory(flavorProguardDir)
|
||||||
|
} else {
|
||||||
|
logger.warn("Proguard folder ${File(projectDir, flavorProguardDir).absolutePath} does not exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Assigns a different version code for each output APK
|
// Assigns a different version code for each output APK
|
||||||
// other than the universal APK.
|
// other than the universal APK.
|
||||||
variant.outputs.forEach { output ->
|
variant.outputs.forEach { output ->
|
||||||
@@ -259,7 +270,6 @@ dependencies {
|
|||||||
if (isEnterpriseBuild) {
|
if (isEnterpriseBuild) {
|
||||||
allEnterpriseImpl(project)
|
allEnterpriseImpl(project)
|
||||||
implementation(projects.appicon.enterprise)
|
implementation(projects.appicon.enterprise)
|
||||||
implementation(project(":enterprise:features:enterprise:shared"))
|
|
||||||
} else {
|
} else {
|
||||||
implementation(projects.features.enterprise.implFoss)
|
implementation(projects.features.enterprise.implFoss)
|
||||||
implementation(projects.appicon.element)
|
implementation(projects.appicon.element)
|
||||||
|
|||||||
+2
-2
@@ -38,7 +38,7 @@ import java.io.File
|
|||||||
|
|
||||||
@BindingContainer
|
@BindingContainer
|
||||||
@ContributesTo(AppScope::class)
|
@ContributesTo(AppScope::class)
|
||||||
object AppModule {
|
object AppBindingContainer {
|
||||||
@Provides
|
@Provides
|
||||||
@BaseDirectory
|
@BaseDirectory
|
||||||
fun providesBaseDirectory(@ApplicationContext context: Context): File {
|
fun providesBaseDirectory(@ApplicationContext context: Context): File {
|
||||||
@@ -111,7 +111,7 @@ object AppModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@SingleIn(AppScope::class)
|
@SingleIn(AppScope::class)
|
||||||
fun provideSnackbarDispatcher(): SnackbarDispatcher {
|
fun providesSnackbarDispatcher(): SnackbarDispatcher {
|
||||||
return SnackbarDispatcher()
|
return SnackbarDispatcher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2023-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.appconfig
|
|
||||||
|
|
||||||
object PushConfig {
|
|
||||||
/**
|
|
||||||
* Note: pusher_app_id cannot exceed 64 chars.
|
|
||||||
*/
|
|
||||||
const val PUSHER_APP_ID: String = "im.vector.app.android"
|
|
||||||
}
|
|
||||||
@@ -61,17 +61,23 @@ dependencies {
|
|||||||
testCommonDependencies(libs)
|
testCommonDependencies(libs)
|
||||||
testImplementation(projects.features.login.test)
|
testImplementation(projects.features.login.test)
|
||||||
testImplementation(projects.features.share.test)
|
testImplementation(projects.features.share.test)
|
||||||
|
testImplementation(projects.libraries.accountselect.test)
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.libraries.matrixmedia.test)
|
||||||
testImplementation(projects.libraries.oauth.test)
|
testImplementation(projects.libraries.oauth.test)
|
||||||
testImplementation(projects.libraries.permissions.test)
|
testImplementation(projects.libraries.permissions.test)
|
||||||
testImplementation(projects.libraries.preferences.test)
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.libraries.pushproviders.test)
|
testImplementation(projects.libraries.pushproviders.test)
|
||||||
|
testImplementation(projects.libraries.sessionStorage.test)
|
||||||
|
testImplementation(projects.features.announcement.test)
|
||||||
testImplementation(projects.features.forward.test)
|
testImplementation(projects.features.forward.test)
|
||||||
testImplementation(projects.features.messages.test)
|
testImplementation(projects.features.messages.test)
|
||||||
testImplementation(projects.features.networkmonitor.test)
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
|
testImplementation(projects.features.preferences.test)
|
||||||
testImplementation(projects.features.rageshake.test)
|
testImplementation(projects.features.rageshake.test)
|
||||||
|
testImplementation(projects.features.signedout.test)
|
||||||
testImplementation(projects.services.apperror.test)
|
testImplementation(projects.services.apperror.test)
|
||||||
testImplementation(projects.services.appnavstate.test)
|
testImplementation(projects.services.appnavstate.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
|
|||||||
@@ -335,11 +335,11 @@ class LoggedInFlowNode(
|
|||||||
}
|
}
|
||||||
NavTarget.Home -> {
|
NavTarget.Home -> {
|
||||||
val callback = object : HomeEntryPoint.Callback {
|
val callback = object : HomeEntryPoint.Callback {
|
||||||
override fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) {
|
override fun navigateToRoom(roomId: RoomId, eventId: EventId?, joinedRoom: JoinedRoom?) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
attachRoom(
|
attachRoom(
|
||||||
roomIdOrAlias = roomId.toRoomIdOrAlias(),
|
roomIdOrAlias = roomId.toRoomIdOrAlias(),
|
||||||
initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom),
|
initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom, eventId = eventId),
|
||||||
clearBackstack = false,
|
clearBackstack = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import dev.zacsweers.metro.Assisted
|
|||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
import io.element.android.annotations.ContributesNode
|
import io.element.android.annotations.ContributesNode
|
||||||
import io.element.android.appnav.di.MatrixSessionCache
|
|
||||||
import io.element.android.appnav.intent.IntentResolver
|
import io.element.android.appnav.intent.IntentResolver
|
||||||
import io.element.android.appnav.intent.ResolvedIntent
|
import io.element.android.appnav.intent.ResolvedIntent
|
||||||
import io.element.android.appnav.room.RoomFlowNode
|
import io.element.android.appnav.room.RoomFlowNode
|
||||||
@@ -40,6 +39,7 @@ import io.element.android.appnav.room.RoomNavigationTarget
|
|||||||
import io.element.android.appnav.root.RootNavStateFlowFactory
|
import io.element.android.appnav.root.RootNavStateFlowFactory
|
||||||
import io.element.android.appnav.root.RootPresenter
|
import io.element.android.appnav.root.RootPresenter
|
||||||
import io.element.android.appnav.root.RootView
|
import io.element.android.appnav.root.RootView
|
||||||
|
import io.element.android.appnav.session.MatrixSessionCache
|
||||||
import io.element.android.features.announcement.api.AnnouncementService
|
import io.element.android.features.announcement.api.AnnouncementService
|
||||||
import io.element.android.features.login.api.LoginParams
|
import io.element.android.features.login.api.LoginParams
|
||||||
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
|
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
|
||||||
@@ -109,6 +109,13 @@ class RootFlowNode(
|
|||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = plugins
|
plugins = plugins
|
||||||
) {
|
) {
|
||||||
|
/**
|
||||||
|
* Login params coming from a launch or new [Intent], waiting to be consumed by the not logged in flow.
|
||||||
|
* Kept here so that the root nav target can be computed from both the logged in state and the pending
|
||||||
|
* login params, whatever the order in which the intent and the first nav state emission are processed.
|
||||||
|
*/
|
||||||
|
private var pendingLoginParams: LoginParams? = null
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
analyticsColdStartWatcher.start()
|
analyticsColdStartWatcher.start()
|
||||||
appCoroutineScope.launch {
|
appCoroutineScope.launch {
|
||||||
@@ -152,7 +159,7 @@ class RootFlowNode(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
LoggedInState.NotLoggedIn -> {
|
LoggedInState.NotLoggedIn -> {
|
||||||
switchToNotLoggedInFlow(null)
|
switchToNotLoggedInFlow(pendingLoginParams)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,15 +201,18 @@ class RootFlowNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) {
|
private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) {
|
||||||
|
pendingLoginParams = null
|
||||||
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId))
|
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun switchToNotLoggedInFlow(params: LoginParams?) {
|
private fun switchToNotLoggedInFlow(params: LoginParams?) {
|
||||||
|
Timber.d("switchToNotLoggedInFlow, hasLoginParams=${params != null}")
|
||||||
matrixSessionCache.removeAll()
|
matrixSessionCache.removeAll()
|
||||||
backstack.safeRoot(NavTarget.NotLoggedInFlow(params))
|
backstack.safeRoot(NavTarget.NotLoggedInFlow(params))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun switchToSignedOutFlow(sessionId: SessionId) {
|
private fun switchToSignedOutFlow(sessionId: SessionId) {
|
||||||
|
pendingLoginParams = null
|
||||||
backstack.safeRoot(NavTarget.SignedOutFlow(sessionId))
|
backstack.safeRoot(NavTarget.SignedOutFlow(sessionId))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,6 +329,7 @@ class RootFlowNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDone() {
|
override fun onDone() {
|
||||||
|
pendingLoginParams = null
|
||||||
backstack.pop()
|
backstack.pop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,6 +429,7 @@ class RootFlowNode(
|
|||||||
Timber.w("Login link ignored, multi account is disabled")
|
Timber.w("Login link ignored, multi account is disabled")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
pendingLoginParams = params
|
||||||
switchToNotLoggedInFlow(params)
|
switchToNotLoggedInFlow(params)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ class IntentResolver(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun Intent.canBeIgnored(): Boolean {
|
private fun Intent.canBeIgnored(): Boolean {
|
||||||
|
if (flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) return true
|
||||||
return action == Intent.ACTION_MAIN &&
|
return action == Intent.ACTION_MAIN &&
|
||||||
categories?.contains(Intent.CATEGORY_LAUNCHER) == true
|
categories?.contains(Intent.CATEGORY_LAUNCHER) == true
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -8,11 +8,11 @@
|
|||||||
|
|
||||||
package io.element.android.appnav.loggedin
|
package io.element.android.appnav.loggedin
|
||||||
|
|
||||||
sealed interface LoggedInEvents {
|
sealed interface LoggedInEvent {
|
||||||
data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvents
|
data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvent
|
||||||
data object CheckSlidingSyncProxyAvailability : LoggedInEvents
|
data object CheckSlidingSyncProxyAvailability : LoggedInEvent
|
||||||
data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvents
|
data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvent
|
||||||
data object DismissLocalNetworkPermissionPrompt : LoggedInEvents
|
data object DismissLocalNetworkPermissionPrompt : LoggedInEvent
|
||||||
|
|
||||||
data object RequestLocationNetworkPermission : LoggedInEvents
|
data object RequestLocationNetworkPermission : LoggedInEvent
|
||||||
}
|
}
|
||||||
@@ -30,8 +30,6 @@ import io.element.android.libraries.architecture.Presenter
|
|||||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
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 io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
@@ -67,7 +65,6 @@ class LoggedInPresenter(
|
|||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor,
|
private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor,
|
||||||
private val featureFlagService: FeatureFlagService,
|
|
||||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||||
) : Presenter<LoggedInState> {
|
) : Presenter<LoggedInState> {
|
||||||
private val localNetworkPermissionsPresenter: PermissionsPresenter =
|
private val localNetworkPermissionsPresenter: PermissionsPresenter =
|
||||||
@@ -124,13 +121,9 @@ class LoggedInPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
// Keep automatic call status (m.call) in sync with the feature flag and homeserver support.
|
// Keep automatic call status (m.call) in sync with homeserver support.
|
||||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.UserStatus)
|
val enabled = matrixClient.isUserStatusSupported().getOrDefault(false)
|
||||||
.onEach { isUserStatusEnabled ->
|
matrixClient.enableAutomaticCallStatus(enabled)
|
||||||
val enabled = isUserStatusEnabled && matrixClient.isUserStatusSupported().getOrDefault(false)
|
|
||||||
matrixClient.enableAutomaticCallStatus(enabled)
|
|
||||||
}
|
|
||||||
.launchIn(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val networkConnectivity by networkMonitor.connectivity.collectAsState()
|
val networkConnectivity by networkMonitor.connectivity.collectAsState()
|
||||||
@@ -156,9 +149,9 @@ class LoggedInPresenter(
|
|||||||
else -> LocalNetworkPermissionDialog.Settings
|
else -> LocalNetworkPermissionDialog.Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleEvent(event: LoggedInEvents) {
|
fun handleEvent(event: LoggedInEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is LoggedInEvents.CloseErrorDialog -> {
|
is LoggedInEvent.CloseErrorDialog -> {
|
||||||
pusherRegistrationState.value = AsyncData.Uninitialized
|
pusherRegistrationState.value = AsyncData.Uninitialized
|
||||||
if (event.doNotShowAgain) {
|
if (event.doNotShowAgain) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
@@ -166,17 +159,17 @@ class LoggedInPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LoggedInEvents.CheckSlidingSyncProxyAvailability -> coroutineScope.launch {
|
LoggedInEvent.CheckSlidingSyncProxyAvailability -> coroutineScope.launch {
|
||||||
forceNativeSlidingSyncMigration = matrixClient.needsForcedNativeSlidingSyncMigration().getOrDefault(false)
|
forceNativeSlidingSyncMigration = matrixClient.needsForcedNativeSlidingSyncMigration().getOrDefault(false)
|
||||||
}
|
}
|
||||||
LoggedInEvents.LogoutAndMigrateToNativeSlidingSync -> coroutineScope.launch {
|
LoggedInEvent.LogoutAndMigrateToNativeSlidingSync -> coroutineScope.launch {
|
||||||
// Force the logout since Native Sliding Sync is already enforced by the SDK
|
// Force the logout since Native Sliding Sync is already enforced by the SDK
|
||||||
matrixClient.logout(userInitiated = true, ignoreSdkError = true)
|
matrixClient.logout(userInitiated = true, ignoreSdkError = true)
|
||||||
}
|
}
|
||||||
LoggedInEvents.DismissLocalNetworkPermissionPrompt -> {
|
LoggedInEvent.DismissLocalNetworkPermissionPrompt -> {
|
||||||
localNetworkPromptDismissedThisSession = true
|
localNetworkPromptDismissedThisSession = true
|
||||||
}
|
}
|
||||||
LoggedInEvents.RequestLocationNetworkPermission -> {
|
LoggedInEvent.RequestLocationNetworkPermission -> {
|
||||||
if (localNetworkPermissionDialog == LocalNetworkPermissionDialog.Settings) {
|
if (localNetworkPermissionDialog == LocalNetworkPermissionDialog.Settings) {
|
||||||
localNetworkPermissionState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog)
|
localNetworkPermissionState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -18,5 +18,5 @@ data class LoggedInState(
|
|||||||
val forceNativeSlidingSyncMigration: Boolean,
|
val forceNativeSlidingSyncMigration: Boolean,
|
||||||
val appName: String,
|
val appName: String,
|
||||||
val localNetworkPermissionDialog: LocalNetworkPermissionDialog,
|
val localNetworkPermissionDialog: LocalNetworkPermissionDialog,
|
||||||
val eventSink: (LoggedInEvents) -> Unit,
|
val eventSink: (LoggedInEvent) -> Unit,
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ import io.element.android.libraries.architecture.AsyncData
|
|||||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||||
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
import io.element.android.libraries.push.api.PusherRegistrationFailure
|
||||||
|
|
||||||
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
|
open class LoggedInStatePreviewParam : PreviewParameterProvider<LoggedInState> {
|
||||||
override val values: Sequence<LoggedInState>
|
override val values: Sequence<LoggedInState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aLoggedInState(),
|
aLoggedInState(),
|
||||||
@@ -37,7 +37,7 @@ fun LoggedInView(
|
|||||||
) {
|
) {
|
||||||
OnLifecycleEvent { _, event ->
|
OnLifecycleEvent { _, event ->
|
||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
state.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
|
state.eventSink(LoggedInEvent.CheckSlidingSyncProxyAvailability)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
@@ -63,10 +63,10 @@ fun LoggedInView(
|
|||||||
content = stringResource(id = CommonStrings.common_error_registering_pusher_android, reason),
|
content = stringResource(id = CommonStrings.common_error_registering_pusher_android, reason),
|
||||||
cancelText = stringResource(id = CommonStrings.common_settings),
|
cancelText = stringResource(id = CommonStrings.common_settings),
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
state.eventSink(LoggedInEvents.CloseErrorDialog(it))
|
state.eventSink(LoggedInEvent.CloseErrorDialog(it))
|
||||||
},
|
},
|
||||||
onCancel = {
|
onCancel = {
|
||||||
state.eventSink(LoggedInEvents.CloseErrorDialog(false))
|
state.eventSink(LoggedInEvent.CloseErrorDialog(false))
|
||||||
navigateToNotificationTroubleshoot()
|
navigateToNotificationTroubleshoot()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -79,15 +79,15 @@ fun LoggedInView(
|
|||||||
ForceNativeSlidingSyncMigrationDialog(
|
ForceNativeSlidingSyncMigrationDialog(
|
||||||
appName = state.appName,
|
appName = state.appName,
|
||||||
onSubmit = {
|
onSubmit = {
|
||||||
state.eventSink(LoggedInEvents.LogoutAndMigrateToNativeSlidingSync)
|
state.eventSink(LoggedInEvent.LogoutAndMigrateToNativeSlidingSync)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalNetworkPermissionDialogView(
|
LocalNetworkPermissionDialogView(
|
||||||
dialog = state.localNetworkPermissionDialog,
|
dialog = state.localNetworkPermissionDialog,
|
||||||
onSubmit = { state.eventSink(LoggedInEvents.RequestLocationNetworkPermission) },
|
onSubmit = { state.eventSink(LoggedInEvent.RequestLocationNetworkPermission) },
|
||||||
onDismiss = { state.eventSink(LoggedInEvents.DismissLocalNetworkPermissionPrompt) },
|
onDismiss = { state.eventSink(LoggedInEvent.DismissLocalNetworkPermissionPrompt) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ private fun ForceNativeSlidingSyncMigrationDialog(
|
|||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun LoggedInViewPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) = ElementPreview {
|
internal fun LoggedInViewPreview(@PreviewParameter(LoggedInStatePreviewParam::class) state: LoggedInState) = ElementPreview {
|
||||||
LoggedInView(
|
LoggedInView(
|
||||||
state = state,
|
state = state,
|
||||||
navigateToNotificationTroubleshoot = {},
|
navigateToNotificationTroubleshoot = {},
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.designsystem.utils.DelayedVisibility
|
import io.element.android.libraries.designsystem.utils.DelayedVisibility
|
||||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
||||||
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateProvider
|
import io.element.android.libraries.matrix.ui.room.LoadingRoomStatePreviewParam
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -84,7 +84,7 @@ private fun LoadingRoomTopBar(
|
|||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) = ElementPreview {
|
internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStatePreviewParam::class) state: LoadingRoomState) = ElementPreview {
|
||||||
LoadingRoomNodeView(
|
LoadingRoomNodeView(
|
||||||
state = state,
|
state = state,
|
||||||
onBackClick = {}
|
onBackClick = {}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ package io.element.android.appnav.root
|
|||||||
import com.bumble.appyx.core.state.MutableSavedStateMap
|
import com.bumble.appyx.core.state.MutableSavedStateMap
|
||||||
import com.bumble.appyx.core.state.SavedStateMap
|
import com.bumble.appyx.core.state.SavedStateMap
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.appnav.di.MatrixSessionCache
|
import io.element.android.appnav.session.MatrixSessionCache
|
||||||
import io.element.android.features.preferences.api.CacheService
|
import io.element.android.features.preferences.api.CacheService
|
||||||
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
|
||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ import io.element.android.features.rageshake.api.detection.aRageshakeDetectionSt
|
|||||||
import io.element.android.services.apperror.api.AppErrorState
|
import io.element.android.services.apperror.api.AppErrorState
|
||||||
import io.element.android.services.apperror.api.aAppErrorState
|
import io.element.android.services.apperror.api.aAppErrorState
|
||||||
|
|
||||||
open class RootStateProvider : PreviewParameterProvider<RootState> {
|
open class RootStatePreviewParam : PreviewParameterProvider<RootState> {
|
||||||
override val values: Sequence<RootState>
|
override val values: Sequence<RootState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aRootState().copy(
|
aRootState().copy(
|
||||||
@@ -60,7 +60,7 @@ fun RootView(
|
|||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RootViewPreview(@PreviewParameter(RootStateProvider::class) rootState: RootState) = ElementPreview {
|
internal fun RootViewPreview(@PreviewParameter(RootStatePreviewParam::class) rootState: RootState) = ElementPreview {
|
||||||
RootView(
|
RootView(
|
||||||
state = rootState,
|
state = rootState,
|
||||||
onOpenBugReport = {},
|
onOpenBugReport = {},
|
||||||
|
|||||||
+2
-2
@@ -6,7 +6,7 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.appnav.di
|
package io.element.android.appnav.session
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import com.bumble.appyx.core.state.MutableSavedStateMap
|
import com.bumble.appyx.core.state.MutableSavedStateMap
|
||||||
@@ -115,7 +115,7 @@ class MatrixSessionCache(
|
|||||||
|
|
||||||
private fun onNewMatrixClient(matrixClient: MatrixClient) {
|
private fun onNewMatrixClient(matrixClient: MatrixClient) {
|
||||||
val syncOrchestrator = syncOrchestratorFactory.create(
|
val syncOrchestrator = syncOrchestratorFactory.create(
|
||||||
syncService = matrixClient.syncService,
|
matrixClient = matrixClient,
|
||||||
sessionCoroutineScope = matrixClient.sessionCoroutineScope,
|
sessionCoroutineScope = matrixClient.sessionCoroutineScope,
|
||||||
)
|
)
|
||||||
sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
|
sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
|
||||||
+12
-5
@@ -6,7 +6,7 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.appnav.di
|
package io.element.android.appnav.session
|
||||||
|
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
@@ -16,7 +16,7 @@ import io.element.android.features.networkmonitor.api.NetworkMonitor
|
|||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.coroutine.childScope
|
import io.element.android.libraries.core.coroutine.childScope
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analytics.api.recordTransaction
|
import io.element.android.services.analytics.api.recordTransaction
|
||||||
@@ -37,7 +37,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
|
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class SyncOrchestrator(
|
class SyncOrchestrator(
|
||||||
@Assisted private val syncService: SyncService,
|
@Assisted private val matrixClient: MatrixClient,
|
||||||
@Assisted sessionCoroutineScope: CoroutineScope,
|
@Assisted sessionCoroutineScope: CoroutineScope,
|
||||||
private val appForegroundStateService: AppForegroundStateService,
|
private val appForegroundStateService: AppForegroundStateService,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
@@ -47,13 +47,15 @@ class SyncOrchestrator(
|
|||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(
|
fun create(
|
||||||
syncService: SyncService,
|
matrixClient: MatrixClient,
|
||||||
sessionCoroutineScope: CoroutineScope,
|
sessionCoroutineScope: CoroutineScope,
|
||||||
): SyncOrchestrator
|
): SyncOrchestrator
|
||||||
}
|
}
|
||||||
|
|
||||||
private val tag = "SyncOrchestrator"
|
private val tag = "SyncOrchestrator"
|
||||||
|
|
||||||
|
private val syncService = matrixClient.syncService
|
||||||
|
|
||||||
private val coroutineScope = sessionCoroutineScope.childScope(dispatchers.io, tag)
|
private val coroutineScope = sessionCoroutineScope.childScope(dispatchers.io, tag)
|
||||||
|
|
||||||
private val started = AtomicBoolean(false)
|
private val started = AtomicBoolean(false)
|
||||||
@@ -94,7 +96,7 @@ class SyncOrchestrator(
|
|||||||
appForegroundStateService.isInCall,
|
appForegroundStateService.isInCall,
|
||||||
appForegroundStateService.isSyncingNotificationEvent,
|
appForegroundStateService.isSyncingNotificationEvent,
|
||||||
appForegroundStateService.hasRingingCall,
|
appForegroundStateService.hasRingingCall,
|
||||||
appForegroundStateService.isSharingLiveLocation
|
appForegroundStateService.isSharingLiveLocation,
|
||||||
)
|
)
|
||||||
val isAppActiveFlow = combine(isAppActiveFlows) { actives -> actives.any { it } }
|
val isAppActiveFlow = combine(isAppActiveFlows) { actives -> actives.any { it } }
|
||||||
combine(
|
combine(
|
||||||
@@ -103,6 +105,11 @@ class SyncOrchestrator(
|
|||||||
networkMonitor.connectivity,
|
networkMonitor.connectivity,
|
||||||
isAppActiveFlow,
|
isAppActiveFlow,
|
||||||
) { syncState, networkState, isAppActive ->
|
) { syncState, networkState, isAppActive ->
|
||||||
|
if (matrixClient.isShuttingDown) {
|
||||||
|
Timber.tag(tag).d("Matrix client is shutting down, no need to start the sync service again")
|
||||||
|
return@combine SyncStateAction.NoOp
|
||||||
|
}
|
||||||
|
|
||||||
val isNetworkAvailable = networkState == NetworkStatus.Connected
|
val isNetworkAvailable = networkState == NetworkStatus.Connected
|
||||||
|
|
||||||
Timber.tag(tag).d("isAppActive=$isAppActive, isNetworkAvailable=$isNetworkAvailable")
|
Timber.tag(tag).d("isAppActive=$isAppActive, isNetworkAvailable=$isNetworkAvailable")
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
|
package io.element.android.appnav
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
|
import com.bumble.appyx.core.modality.AncestryInfo
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.node
|
||||||
|
import com.bumble.appyx.navmodel.backstack.activeElement
|
||||||
|
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
|
||||||
|
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
|
||||||
|
import com.bumble.appyx.utils.customisations.NodeCustomisationDirectoryImpl
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.appnav.intent.IntentResolver
|
||||||
|
import io.element.android.appnav.root.RootNavStateFlowFactory
|
||||||
|
import io.element.android.appnav.root.RootPresenter
|
||||||
|
import io.element.android.appnav.session.FakeSyncOrchestratorFactory
|
||||||
|
import io.element.android.appnav.session.MatrixSessionCache
|
||||||
|
import io.element.android.features.login.api.LoginEntryPoint
|
||||||
|
import io.element.android.features.login.api.LoginParams
|
||||||
|
import io.element.android.features.login.test.FakeLoginEntryPoint
|
||||||
|
import io.element.android.features.login.test.FakeLoginIntentResolver
|
||||||
|
import io.element.android.features.login.test.accesscontrol.FakeAccountProviderAccessControl
|
||||||
|
import io.element.android.features.preferences.test.FakeCacheService
|
||||||
|
import io.element.android.features.rageshake.test.FakeBugReportEntryPoint
|
||||||
|
import io.element.android.features.rageshake.test.logs.FakeAnnouncementService
|
||||||
|
import io.element.android.features.share.test.FakeShareIntentHandler
|
||||||
|
import io.element.android.features.signedout.test.FakeSignedOutEntryPoint
|
||||||
|
import io.element.android.libraries.accountselect.test.FakeAccountSelectEntryPoint
|
||||||
|
import io.element.android.libraries.architecture.AssistedNodeFactory
|
||||||
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
|
import io.element.android.libraries.matrix.test.FakeSdkMetadata
|
||||||
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
|
import io.element.android.libraries.matrix.ui.media.test.FakeImageLoaderHolder
|
||||||
|
import io.element.android.libraries.oauth.test.FakeOAuthActionFlow
|
||||||
|
import io.element.android.libraries.oauth.test.FakeOAuthIntentResolver
|
||||||
|
import io.element.android.libraries.preferences.test.FakeSessionPreferencesStoreFactory
|
||||||
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
|
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
||||||
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
|
import io.element.android.services.analytics.test.watchers.FakeAnalyticsColdStartWatcher
|
||||||
|
import io.element.android.services.apperror.test.FakeAppErrorStateService
|
||||||
|
import io.element.android.tests.testutils.node.FakeNodeFactoriesBindings
|
||||||
|
import io.element.android.tests.testutils.node.FakeParentNode
|
||||||
|
import io.element.android.tests.testutils.presenter.NotUsedPresenter
|
||||||
|
import io.element.android.tests.testutils.robolectric.RobolectricTest
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.runCurrent
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private const val A_LOGIN_LINK = "https://mobile.element.io/element/?account_provider=example.com&login_hint=mxid:@alice:example.com"
|
||||||
|
|
||||||
|
private val A_LOGIN_PARAMS = LoginParams(
|
||||||
|
accountProvider = "example.com",
|
||||||
|
loginHint = "mxid:@alice:example.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
private val A_LOGIN_ENTRY_POINT_PARAMS = LoginEntryPoint.Params(
|
||||||
|
accountProvider = A_LOGIN_PARAMS.accountProvider,
|
||||||
|
loginHint = A_LOGIN_PARAMS.loginHint,
|
||||||
|
)
|
||||||
|
|
||||||
|
class RootFlowNodeTest : RobolectricTest() {
|
||||||
|
@get:Rule
|
||||||
|
val instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val mainDispatcherRule = MainDispatcherRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given no session, when a login link is handled before the nav state is observed, then the login params are kept`() = runTest {
|
||||||
|
var loginEntryPointParams: LoginEntryPoint.Params? = null
|
||||||
|
val rootFlowNode = createRootFlowNode(
|
||||||
|
loginEntryPoint = FakeLoginEntryPoint { buildContext, params ->
|
||||||
|
loginEntryPointParams = params
|
||||||
|
node(buildContext) {}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
rootFlowNode.parentNodeTestHelper()
|
||||||
|
// The intent is handled first, this is what happens when the deep link cold starts the app.
|
||||||
|
rootFlowNode.handleIntent(aLoginIntent())
|
||||||
|
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(A_LOGIN_PARAMS))
|
||||||
|
// Then the first nav state emission lands, it must not override the login params.
|
||||||
|
runCurrent()
|
||||||
|
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(A_LOGIN_PARAMS))
|
||||||
|
assertThat(loginEntryPointParams).isEqualTo(A_LOGIN_ENTRY_POINT_PARAMS)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given no session, when a login link is handled after the nav state is observed, then the login params are applied`() = runTest {
|
||||||
|
var loginEntryPointParams: LoginEntryPoint.Params? = null
|
||||||
|
val rootFlowNode = createRootFlowNode(
|
||||||
|
loginEntryPoint = FakeLoginEntryPoint { buildContext, params ->
|
||||||
|
loginEntryPointParams = params
|
||||||
|
node(buildContext) {}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
rootFlowNode.parentNodeTestHelper()
|
||||||
|
// The nav state is observed first, this is what happens when the app is already running.
|
||||||
|
runCurrent()
|
||||||
|
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(null))
|
||||||
|
rootFlowNode.handleIntent(aLoginIntent())
|
||||||
|
runCurrent()
|
||||||
|
assertThat(rootFlowNode.backstack.activeElement).isEqualTo(RootFlowNode.NavTarget.NotLoggedInFlow(A_LOGIN_PARAMS))
|
||||||
|
assertThat(loginEntryPointParams).isEqualTo(A_LOGIN_ENTRY_POINT_PARAMS)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aLoginIntent() = Intent(Intent.ACTION_VIEW, Uri.parse(A_LOGIN_LINK))
|
||||||
|
|
||||||
|
private fun TestScope.createRootFlowNode(
|
||||||
|
loginEntryPoint: LoginEntryPoint,
|
||||||
|
sessionStore: SessionStore = InMemorySessionStore(),
|
||||||
|
): RootFlowNode {
|
||||||
|
val matrixSessionCache = MatrixSessionCache(
|
||||||
|
authenticationService = FakeMatrixAuthenticationService(),
|
||||||
|
syncOrchestratorFactory = FakeSyncOrchestratorFactory(),
|
||||||
|
analyticsService = FakeAnalyticsService(),
|
||||||
|
)
|
||||||
|
val parentNode = FakeParentNode(
|
||||||
|
graph = FakeNodeFactoriesBindings(
|
||||||
|
mapOf(
|
||||||
|
NotLoggedInFlowNode::class to AssistedNodeFactory { buildContext, plugins ->
|
||||||
|
NotLoggedInFlowNode(
|
||||||
|
buildContext = buildContext,
|
||||||
|
plugins = plugins,
|
||||||
|
loginEntryPoint = loginEntryPoint,
|
||||||
|
imageLoaderHolder = FakeImageLoaderHolder(),
|
||||||
|
analyticsColdStartWatcher = FakeAnalyticsColdStartWatcher(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return RootFlowNode(
|
||||||
|
buildContext = BuildContext(
|
||||||
|
ancestryInfo = AncestryInfo.Child(anchor = parentNode),
|
||||||
|
savedStateMap = null,
|
||||||
|
customisations = NodeCustomisationDirectoryImpl(),
|
||||||
|
),
|
||||||
|
plugins = emptyList(),
|
||||||
|
sessionStore = sessionStore,
|
||||||
|
accountProviderAccessControl = FakeAccountProviderAccessControl { true },
|
||||||
|
navStateFlowFactory = RootNavStateFlowFactory(
|
||||||
|
sessionStore = sessionStore,
|
||||||
|
cacheService = FakeCacheService(),
|
||||||
|
matrixSessionCache = matrixSessionCache,
|
||||||
|
imageLoaderHolder = FakeImageLoaderHolder(),
|
||||||
|
sessionPreferencesStoreFactory = FakeSessionPreferencesStoreFactory(),
|
||||||
|
),
|
||||||
|
matrixSessionCache = matrixSessionCache,
|
||||||
|
presenter = RootPresenter(
|
||||||
|
crashDetectionPresenter = NotUsedPresenter(),
|
||||||
|
rageshakeDetectionPresenter = NotUsedPresenter(),
|
||||||
|
appErrorStateService = FakeAppErrorStateService(),
|
||||||
|
analyticsService = FakeAnalyticsService(),
|
||||||
|
sdkMetadata = FakeSdkMetadata("sha"),
|
||||||
|
),
|
||||||
|
bugReportEntryPoint = FakeBugReportEntryPoint(),
|
||||||
|
signedOutEntryPoint = FakeSignedOutEntryPoint(),
|
||||||
|
accountSelectEntryPoint = FakeAccountSelectEntryPoint(),
|
||||||
|
intentResolver = IntentResolver(
|
||||||
|
deeplinkParser = { null },
|
||||||
|
loginIntentResolver = FakeLoginIntentResolver { A_LOGIN_PARAMS },
|
||||||
|
oAuthIntentResolver = FakeOAuthIntentResolver { null },
|
||||||
|
permalinkParser = FakePermalinkParser(),
|
||||||
|
shareIntentHandler = FakeShareIntentHandler(),
|
||||||
|
),
|
||||||
|
oAuthActionFlow = FakeOAuthActionFlow(),
|
||||||
|
featureFlagService = FakeFeatureFlagService(),
|
||||||
|
announcementService = FakeAnnouncementService(),
|
||||||
|
analyticsService = FakeAnalyticsService(),
|
||||||
|
analyticsColdStartWatcher = FakeAnalyticsColdStartWatcher(),
|
||||||
|
appCoroutineScope = backgroundScope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,10 +8,11 @@
|
|||||||
|
|
||||||
package io.element.android.appnav
|
package io.element.android.appnav
|
||||||
|
|
||||||
import io.element.android.appnav.di.SyncOrchestrator
|
import io.element.android.appnav.session.SyncOrchestrator
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
|
||||||
@@ -40,7 +41,7 @@ class SyncOrchestratorTest {
|
|||||||
}
|
}
|
||||||
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
|
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ class SyncOrchestratorTest {
|
|||||||
}
|
}
|
||||||
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
|
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -85,7 +86,7 @@ class SyncOrchestratorTest {
|
|||||||
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
|
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
|
||||||
val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true)
|
val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
)
|
)
|
||||||
@@ -117,7 +118,7 @@ class SyncOrchestratorTest {
|
|||||||
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
|
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
|
||||||
val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true)
|
val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
)
|
)
|
||||||
@@ -165,7 +166,7 @@ class SyncOrchestratorTest {
|
|||||||
initialIsSyncingNotificationEventValue = false,
|
initialIsSyncingNotificationEventValue = false,
|
||||||
)
|
)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
)
|
)
|
||||||
@@ -208,7 +209,7 @@ class SyncOrchestratorTest {
|
|||||||
initialIsSyncingNotificationEventValue = false,
|
initialIsSyncingNotificationEventValue = false,
|
||||||
)
|
)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
)
|
)
|
||||||
@@ -252,7 +253,7 @@ class SyncOrchestratorTest {
|
|||||||
initialHasRingingCall = false,
|
initialHasRingingCall = false,
|
||||||
)
|
)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
)
|
)
|
||||||
@@ -296,7 +297,7 @@ class SyncOrchestratorTest {
|
|||||||
initialIsInCallValue = true,
|
initialIsInCallValue = true,
|
||||||
)
|
)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
)
|
)
|
||||||
@@ -339,7 +340,7 @@ class SyncOrchestratorTest {
|
|||||||
initialIsInCallValue = false,
|
initialIsInCallValue = false,
|
||||||
)
|
)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
)
|
)
|
||||||
@@ -369,7 +370,7 @@ class SyncOrchestratorTest {
|
|||||||
}
|
}
|
||||||
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
|
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
|
||||||
val syncOrchestrator = createSyncOrchestrator(
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = FakeMatrixClient(syncService = syncService),
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -381,12 +382,44 @@ class SyncOrchestratorTest {
|
|||||||
startSyncRecorder.assertions().isNeverCalled()
|
startSyncRecorder.assertions().isNeverCalled()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when the client is shutting down, the orchestrator does nothing`() = runTest {
|
||||||
|
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
|
||||||
|
val appForegroundStateService = FakeAppForegroundStateService(
|
||||||
|
initialForegroundValue = false,
|
||||||
|
initialIsSyncingNotificationEventValue = false,
|
||||||
|
initialIsInCallValue = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
val startSyncRecorder = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
|
||||||
|
val syncService = FakeSyncService(initialSyncState = SyncState.Idle).apply {
|
||||||
|
startSyncLambda = startSyncRecorder
|
||||||
|
}
|
||||||
|
val syncOrchestrator = createSyncOrchestrator(
|
||||||
|
matrixClient = FakeMatrixClient(syncService = syncService, isShuttingDownResult = { true }),
|
||||||
|
networkMonitor = networkMonitor,
|
||||||
|
appForegroundStateService = appForegroundStateService,
|
||||||
|
)
|
||||||
|
|
||||||
|
// We start observing
|
||||||
|
syncOrchestrator.observeStates()
|
||||||
|
|
||||||
|
// These should still not trigger a sync, sync the client is shutting down
|
||||||
|
networkMonitor.givenNetworkBlocked(false)
|
||||||
|
appForegroundStateService.updateIsInCallState(true)
|
||||||
|
appForegroundStateService.isInForeground.value = true
|
||||||
|
|
||||||
|
advanceTimeBy(10.seconds)
|
||||||
|
|
||||||
|
startSyncRecorder.assertions().isNeverCalled()
|
||||||
|
}
|
||||||
|
|
||||||
private fun TestScope.createSyncOrchestrator(
|
private fun TestScope.createSyncOrchestrator(
|
||||||
syncService: FakeSyncService = FakeSyncService(),
|
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
||||||
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
|
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
|
||||||
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
|
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
|
||||||
) = SyncOrchestrator(
|
) = SyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = matrixClient,
|
||||||
sessionCoroutineScope = backgroundScope,
|
sessionCoroutineScope = backgroundScope,
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
appForegroundStateService = appForegroundStateService,
|
appForegroundStateService = appForegroundStateService,
|
||||||
|
|||||||
@@ -45,6 +45,24 @@ class IntentResolverTest : RobolectricTest() {
|
|||||||
assertThat(result).isNull()
|
assertThat(result).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resolve intent replayed from the history should return null`() {
|
||||||
|
val sut = createIntentResolver(
|
||||||
|
deeplinkParserResult = DeeplinkData.Room(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
threadId = null,
|
||||||
|
eventId = null,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val intent = Intent(RuntimeEnvironment.getApplication(), Activity::class.java).apply {
|
||||||
|
action = Intent.ACTION_VIEW
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY)
|
||||||
|
}
|
||||||
|
val result = sut.resolve(intent)
|
||||||
|
assertThat(result).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test resolve navigation intent root`() {
|
fun `test resolve navigation intent root`() {
|
||||||
val sut = createIntentResolver(
|
val sut = createIntentResolver(
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ import im.vector.app.features.analytics.plan.UserProperties
|
|||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
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 io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
@@ -131,7 +128,6 @@ class LoggedInPresenterTest {
|
|||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
||||||
featureFlagService = FakeFeatureFlagService(),
|
|
||||||
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
||||||
).test {
|
).test {
|
||||||
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
|
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
|
||||||
@@ -214,7 +210,7 @@ class LoggedInPresenterTest {
|
|||||||
lambda.assertions()
|
lambda.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
// Reset the error and do not show again
|
// Reset the error and do not show again
|
||||||
finalState.eventSink(LoggedInEvents.CloseErrorDialog(doNotShowAgain = false))
|
finalState.eventSink(LoggedInEvent.CloseErrorDialog(doNotShowAgain = false))
|
||||||
val lastState = awaitItem()
|
val lastState = awaitItem()
|
||||||
assertThat(lastState.pusherRegistrationState.isUninitialized()).isTrue()
|
assertThat(lastState.pusherRegistrationState.isUninitialized()).isTrue()
|
||||||
assertThat(lastState.ignoreRegistrationError).isFalse()
|
assertThat(lastState.ignoreRegistrationError).isFalse()
|
||||||
@@ -244,7 +240,7 @@ class LoggedInPresenterTest {
|
|||||||
lambda.assertions()
|
lambda.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
// Reset the error and do not show again
|
// Reset the error and do not show again
|
||||||
finalState.eventSink(LoggedInEvents.CloseErrorDialog(doNotShowAgain = true))
|
finalState.eventSink(LoggedInEvent.CloseErrorDialog(doNotShowAgain = true))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
setIgnoreRegistrationErrorLambda.assertions()
|
setIgnoreRegistrationErrorLambda.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
@@ -300,7 +296,7 @@ class LoggedInPresenterTest {
|
|||||||
).test {
|
).test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.forceNativeSlidingSyncMigration).isFalse()
|
assertThat(initialState.forceNativeSlidingSyncMigration).isFalse()
|
||||||
initialState.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
|
initialState.eventSink(LoggedInEvent.CheckSlidingSyncProxyAvailability)
|
||||||
assertThat(awaitItem().forceNativeSlidingSyncMigration).isTrue()
|
assertThat(awaitItem().forceNativeSlidingSyncMigration).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,7 +318,7 @@ class LoggedInPresenterTest {
|
|||||||
).test {
|
).test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
||||||
initialState.eventSink(LoggedInEvents.LogoutAndMigrateToNativeSlidingSync)
|
initialState.eventSink(LoggedInEvent.LogoutAndMigrateToNativeSlidingSync)
|
||||||
|
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
@@ -362,9 +358,6 @@ class LoggedInPresenterTest {
|
|||||||
}
|
}
|
||||||
createLoggedInPresenter(
|
createLoggedInPresenter(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
featureFlagService = FakeFeatureFlagService(
|
|
||||||
initialState = mapOf(FeatureFlags.UserStatus.key to true),
|
|
||||||
),
|
|
||||||
).test {
|
).test {
|
||||||
cancelAndConsumeRemainingEvents()
|
cancelAndConsumeRemainingEvents()
|
||||||
assert(enableAutomaticCallStatusLambda)
|
assert(enableAutomaticCallStatusLambda)
|
||||||
@@ -374,7 +367,7 @@ class LoggedInPresenterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - reacts to user status flag being toggled at runtime`() = runTest {
|
fun `present - enables automatic call status when server supports it`() = runTest {
|
||||||
val enableAutomaticCallStatusLambda = lambdaRecorder<Boolean, Unit> { }
|
val enableAutomaticCallStatusLambda = lambdaRecorder<Boolean, Unit> { }
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
accountManagementUrlResult = { Result.success(null) },
|
accountManagementUrlResult = { Result.success(null) },
|
||||||
@@ -382,23 +375,13 @@ class LoggedInPresenterTest {
|
|||||||
).apply {
|
).apply {
|
||||||
isUserStatusSupportedResult = Result.success(true)
|
isUserStatusSupportedResult = Result.success(true)
|
||||||
}
|
}
|
||||||
val featureFlagService = FakeFeatureFlagService(
|
|
||||||
initialState = mapOf(FeatureFlags.UserStatus.key to false),
|
|
||||||
)
|
|
||||||
createLoggedInPresenter(
|
createLoggedInPresenter(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
featureFlagService = featureFlagService,
|
|
||||||
).test {
|
).test {
|
||||||
awaitItem()
|
|
||||||
// Turning the flag on at runtime enables it.
|
|
||||||
featureFlagService.setFeatureEnabled(FeatureFlags.UserStatus, true)
|
|
||||||
cancelAndConsumeRemainingEvents()
|
cancelAndConsumeRemainingEvents()
|
||||||
assert(enableAutomaticCallStatusLambda)
|
assert(enableAutomaticCallStatusLambda)
|
||||||
.isCalledExactly(2)
|
.isCalledOnce()
|
||||||
.withSequence(
|
.with(value(true))
|
||||||
listOf(value(false)),
|
|
||||||
listOf(value(true)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,7 +401,6 @@ class LoggedInPresenterTest {
|
|||||||
),
|
),
|
||||||
buildMeta: BuildMeta = aBuildMeta(),
|
buildMeta: BuildMeta = aBuildMeta(),
|
||||||
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
|
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
|
||||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
|
||||||
): LoggedInPresenter {
|
): LoggedInPresenter {
|
||||||
return LoggedInPresenter(
|
return LoggedInPresenter(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
@@ -430,7 +412,6 @@ class LoggedInPresenterTest {
|
|||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
networkMonitor = networkMonitor,
|
networkMonitor = networkMonitor,
|
||||||
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
||||||
featureFlagService = featureFlagService,
|
|
||||||
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.appnav.session
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
|
class FakeSyncOrchestratorFactory : SyncOrchestrator.Factory {
|
||||||
|
override fun create(
|
||||||
|
matrixClient: MatrixClient,
|
||||||
|
sessionCoroutineScope: CoroutineScope,
|
||||||
|
): SyncOrchestrator = lambdaError()
|
||||||
|
}
|
||||||
+4
-4
@@ -6,12 +6,12 @@
|
|||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.appnav.di
|
package io.element.android.appnav.session
|
||||||
|
|
||||||
import com.bumble.appyx.core.state.MutableSavedStateMapImpl
|
import com.bumble.appyx.core.state.MutableSavedStateMapImpl
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
@@ -138,11 +138,11 @@ class MatrixSessionCacheTest {
|
|||||||
|
|
||||||
return object : SyncOrchestrator.Factory {
|
return object : SyncOrchestrator.Factory {
|
||||||
override fun create(
|
override fun create(
|
||||||
syncService: SyncService,
|
matrixClient: MatrixClient,
|
||||||
sessionCoroutineScope: CoroutineScope,
|
sessionCoroutineScope: CoroutineScope,
|
||||||
): SyncOrchestrator {
|
): SyncOrchestrator {
|
||||||
return SyncOrchestrator(
|
return SyncOrchestrator(
|
||||||
syncService = syncService,
|
matrixClient = matrixClient,
|
||||||
sessionCoroutineScope = sessionCoroutineScope,
|
sessionCoroutineScope = sessionCoroutineScope,
|
||||||
appForegroundStateService = FakeAppForegroundStateService(),
|
appForegroundStateService = FakeAppForegroundStateService(),
|
||||||
networkMonitor = FakeNetworkMonitor(),
|
networkMonitor = FakeNetworkMonitor(),
|
||||||
+1
-1
@@ -98,7 +98,7 @@ allprojects {
|
|||||||
// This is disabled by default, but the CI will enforce this.
|
// This is disabled by default, but the CI will enforce this.
|
||||||
// You can override by passing `-PallWarningsAsErrors=true` in the command line
|
// You can override by passing `-PallWarningsAsErrors=true` in the command line
|
||||||
// Or add a line with "allWarningsAsErrors=true" in your ~/.gradle/gradle.properties file
|
// Or add a line with "allWarningsAsErrors=true" in your ~/.gradle/gradle.properties file
|
||||||
allWarningsAsErrors = project.properties["allWarningsAsErrors"] == "true"
|
allWarningsAsErrors = findProperty("allWarningsAsErrors") == "true"
|
||||||
|
|
||||||
// Uncomment to suppress Compose Kotlin compiler compatibility warning
|
// Uncomment to suppress Compose Kotlin compiler compatibility warning
|
||||||
// freeCompilerArgs.addAll(listOf("-P", "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"))
|
// freeCompilerArgs.addAll(listOf("-P", "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"))
|
||||||
|
|||||||
@@ -61,32 +61,32 @@ class ContributesNodeProcessor(
|
|||||||
if (config.enableLogging) {
|
if (config.enableLogging) {
|
||||||
logger.warn("Processing ${ksClass.qualifiedName?.asString()}")
|
logger.warn("Processing ${ksClass.qualifiedName?.asString()}")
|
||||||
}
|
}
|
||||||
generateModule(ksClass)
|
generateBindingContainer(ksClass)
|
||||||
generateFactory(ksClass)
|
generateFactory(ksClass)
|
||||||
}
|
}
|
||||||
|
|
||||||
return invalidSymbols
|
return invalidSymbols
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateModule(ksClass: KSClassDeclaration) {
|
private fun generateBindingContainer(ksClass: KSClassDeclaration) {
|
||||||
val annotation = ksClass.annotations.find { it.shortName.asString() == "ContributesNode" }!!
|
val annotation = ksClass.annotations.find { it.shortName.asString() == "ContributesNode" }!!
|
||||||
val scope = annotation.arguments.find { it.name?.asString() == "scope" }!!.value as KSType
|
val scope = annotation.arguments.find { it.name?.asString() == "scope" }!!.value as KSType
|
||||||
val modulePackage = ksClass.packageName.asString()
|
val bindingContainerPackage = ksClass.packageName.asString()
|
||||||
val moduleClassName = "${ksClass.simpleName.asString()}_Module"
|
val bindingContainerClassName = "${ksClass.simpleName.asString()}BindingContainer"
|
||||||
val nodeClassName = ClassName.bestGuess(ksClass.qualifiedName!!.asString())
|
val nodeClassName = ClassName.bestGuess(ksClass.qualifiedName!!.asString())
|
||||||
val content = FileSpec.builder(
|
val content = FileSpec.builder(
|
||||||
packageName = modulePackage,
|
packageName = bindingContainerPackage,
|
||||||
fileName = moduleClassName,
|
fileName = bindingContainerClassName,
|
||||||
)
|
)
|
||||||
.addType(
|
.addType(
|
||||||
TypeSpec.interfaceBuilder(moduleClassName)
|
TypeSpec.interfaceBuilder(bindingContainerClassName)
|
||||||
.addAnnotation(AnnotationSpec.builder(Origin::class).addMember(CLASS_PLACEHOLDER, nodeClassName).build())
|
.addAnnotation(AnnotationSpec.builder(Origin::class).addMember(CLASS_PLACEHOLDER, nodeClassName).build())
|
||||||
.addAnnotation(BindingContainer::class)
|
.addAnnotation(BindingContainer::class)
|
||||||
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember(CLASS_PLACEHOLDER, scope.toTypeName()).build())
|
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember(CLASS_PLACEHOLDER, scope.toTypeName()).build())
|
||||||
.addFunction(
|
.addFunction(
|
||||||
FunSpec.builder("bind${ksClass.simpleName.asString()}Factory")
|
FunSpec.builder("bind${ksClass.simpleName.asString()}AssistedFactory")
|
||||||
.addModifiers(KModifier.ABSTRACT)
|
.addModifiers(KModifier.ABSTRACT)
|
||||||
.addParameter("factory", ClassName(modulePackage, "${ksClass.simpleName.asString()}_AssistedFactory"))
|
.addParameter("factory", ClassName(bindingContainerPackage, "${ksClass.simpleName.asString()}AssistedFactory"))
|
||||||
.returns(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(STAR))
|
.returns(ClassName.bestGuess(assistedNodeFactoryFqName.asString()).parameterizedBy(STAR))
|
||||||
.addAnnotation(Binds::class)
|
.addAnnotation(Binds::class)
|
||||||
.addAnnotation(IntoMap::class)
|
.addAnnotation(IntoMap::class)
|
||||||
@@ -114,7 +114,7 @@ class ContributesNodeProcessor(
|
|||||||
@OptIn(KspExperimental::class)
|
@OptIn(KspExperimental::class)
|
||||||
private fun generateFactory(ksClass: KSClassDeclaration) {
|
private fun generateFactory(ksClass: KSClassDeclaration) {
|
||||||
val generatedPackage = ksClass.packageName.asString()
|
val generatedPackage = ksClass.packageName.asString()
|
||||||
val assistedFactoryClassName = "${ksClass.simpleName.asString()}_AssistedFactory"
|
val assistedFactoryClassName = "${ksClass.simpleName.asString()}AssistedFactory"
|
||||||
val constructor = ksClass.getConstructors().first { it.parameters.isNotEmpty() }
|
val constructor = ksClass.getConstructors().first { it.parameters.isNotEmpty() }
|
||||||
val assistedParameters = constructor.parameters.filter { it.isAnnotationPresent(Assisted::class) }
|
val assistedParameters = constructor.parameters.filter { it.isAnnotationPresent(Assisted::class) }
|
||||||
if (assistedParameters.size != 2) {
|
if (assistedParameters.size != 2) {
|
||||||
|
|||||||
@@ -322,10 +322,10 @@ Example for a new feature called RoomDetails:
|
|||||||
4. Verify that the structure looks ok and click on Finish
|
4. Verify that the structure looks ok and click on Finish
|
||||||
5. The modules api/impl should be created under `features/roomdetails` directory.
|
5. The modules api/impl should be created under `features/roomdetails` directory.
|
||||||
6. Sync project with Gradle so the modules are recognized (no need to add them to settings.gradle).
|
6. Sync project with Gradle so the modules are recognized (no need to add them to settings.gradle).
|
||||||
7. You can now add more Presentation classes (Events, State, StateProvider, View, Presenter) in the impl module with the `Template Presentation Classes`.
|
7. You can now add more Presentation classes (Event, State, StatePreviewParam, View, Presenter) in the impl module with the `Template Presentation Classes`.
|
||||||
To use it, just right click on the package where you want to generate classes, and click on `New` / `Template Presentation Classes`.
|
To use it, just right click on the package where you want to generate classes, and click on `New` / `Template Presentation Classes`.
|
||||||
Fill the text field with the base name of the classes: the generated files are `<BaseName>Event.kt`, `<BaseName>State.kt`,
|
Fill the text field with the base name of the classes: the generated files are `<BaseName>Event.kt`, `<BaseName>State.kt`,
|
||||||
`<BaseName>StateProvider.kt`, `<BaseName>Presenter.kt`, `<BaseName>Node.kt` and `<BaseName>View.kt`. For instance
|
`<BaseName>StatePreviewParam.kt`, `<BaseName>Presenter.kt`, `<BaseName>Node.kt` and `<BaseName>View.kt`. For instance
|
||||||
`PreferencesRoot` in the `features/preferences/impl/root` package.
|
`PreferencesRoot` in the `features/preferences/impl/root` package.
|
||||||
|
|
||||||
|
|
||||||
@@ -360,9 +360,9 @@ We have 3 tests frameworks in place, and this should be sufficient to guarantee
|
|||||||
- Maestro to test the global usage of the application. See the related [documentation](../.maestro/README.md).
|
- Maestro to test the global usage of the application. See the related [documentation](../.maestro/README.md).
|
||||||
- Combination of [Showkase](https://github.com/airbnb/Showkase) and [Paparazzi](https://github.com/cashapp/paparazzi), to test UI pixel perfect. To add test,
|
- Combination of [Showkase](https://github.com/airbnb/Showkase) and [Paparazzi](https://github.com/cashapp/paparazzi), to test UI pixel perfect. To add test,
|
||||||
just add `@Preview` for the composable you are adding. See the related [documentation](screenshot_testing.md) and see in the template the
|
just add `@Preview` for the composable you are adding. See the related [documentation](screenshot_testing.md) and see in the template the
|
||||||
file [TemplateView.kt](../features/template/src/main/kotlin/io/element/android/features/template/TemplateView.kt). We create PreviewProvider to provide
|
file [TemplateView.kt](../features/template/src/main/kotlin/io/element/android/features/template/TemplateView.kt). We create PreviewParam classes to provide
|
||||||
different states. See for instance the
|
different states. See for instance the
|
||||||
file [TemplateStateProvider.kt](../features/template/src/main/kotlin/io/element/android/features/template/TemplateStateProvider.kt)
|
file [TemplateStatePreviewParam.kt](../features/template/src/main/kotlin/io/element/android/features/template/TemplateStatePreviewParam.kt)
|
||||||
- Tests on presenter with [Molecule](https://github.com/cashapp/molecule) and [Turbine](https://github.com/cashapp/turbine). See in the template the class [TemplatePresenterTests](../features/template/src/test/kotlin/io/element/android/features/template/TemplatePresenterTests.kt).
|
- Tests on presenter with [Molecule](https://github.com/cashapp/molecule) and [Turbine](https://github.com/cashapp/turbine). See in the template the class [TemplatePresenterTests](../features/template/src/test/kotlin/io/element/android/features/template/TemplatePresenterTests.kt).
|
||||||
|
|
||||||
**Note** For now we want to avoid using class mocking (with library such as *mockk*), because this should be not necessary. We prefer to create Fake
|
**Note** For now we want to avoid using class mocking (with library such as *mockk*), because this should be not necessary. We prefer to create Fake
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ Rust sdk branch: https://github.com/matrix-org/matrix-rust-sdk/tree/oidc-ffi
|
|||||||
|
|
||||||
Figma https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?node-id=133-5426&t=yQXKeANatk6keoZF-0
|
Figma https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?node-id=133-5426&t=yQXKeANatk6keoZF-0
|
||||||
|
|
||||||
Server list: https://github.com/element-hq/oidc-playground
|
|
||||||
|
|
||||||
Metadata iOS: (from https://github.com/element-hq/element-x-ios/blob/5f9d07377cebc4f21d9668b1a25f6e3bb22f64a1/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift#L28)
|
Metadata iOS: (from https://github.com/element-hq/element-x-ios/blob/5f9d07377cebc4f21d9668b1a25f6e3bb22f64a1/ElementX/Sources/Services/Authentication/AuthenticationServiceProxy.swift#L28)
|
||||||
|
|
||||||
clientName: InfoPlistReader.main.bundleDisplayName,
|
clientName: InfoPlistReader.main.bundleDisplayName,
|
||||||
|
|||||||
+1
-1
Submodule enterprise updated: 3bf0df1f25...e633a343b1
@@ -0,0 +1,2 @@
|
|||||||
|
Main changes in this version: bug fixes and improvements.
|
||||||
|
Full changelog: https://github.com/element-hq/element-x-android/releases
|
||||||
+2
-2
@@ -8,6 +8,6 @@
|
|||||||
|
|
||||||
package io.element.android.features.analytics.api
|
package io.element.android.features.analytics.api
|
||||||
|
|
||||||
sealed interface AnalyticsOptInEvents {
|
sealed interface AnalyticsOptInEvent {
|
||||||
data class EnableAnalytics(val isEnabled: Boolean) : AnalyticsOptInEvents
|
data class EnableAnalytics(val isEnabled: Boolean) : AnalyticsOptInEvent
|
||||||
}
|
}
|
||||||
+2
-2
@@ -8,11 +8,11 @@
|
|||||||
|
|
||||||
package io.element.android.features.analytics.api.preferences
|
package io.element.android.features.analytics.api.preferences
|
||||||
|
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
import io.element.android.features.analytics.api.AnalyticsOptInEvent
|
||||||
|
|
||||||
data class AnalyticsPreferencesState(
|
data class AnalyticsPreferencesState(
|
||||||
val applicationName: String,
|
val applicationName: String,
|
||||||
val isEnabled: Boolean,
|
val isEnabled: Boolean,
|
||||||
val policyUrl: String,
|
val policyUrl: String,
|
||||||
val eventSink: (AnalyticsOptInEvents) -> Unit,
|
val eventSink: (AnalyticsOptInEvent) -> Unit,
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ package io.element.android.features.analytics.api.preferences
|
|||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
|
||||||
open class AnalyticsPreferencesStateProvider : PreviewParameterProvider<AnalyticsPreferencesState> {
|
open class AnalyticsPreferencesStatePreviewParam : PreviewParameterProvider<AnalyticsPreferencesState> {
|
||||||
override val values: Sequence<AnalyticsPreferencesState>
|
override val values: Sequence<AnalyticsPreferencesState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aAnalyticsPreferencesState().copy(isEnabled = true),
|
aAnalyticsPreferencesState().copy(isEnabled = true),
|
||||||
+3
-3
@@ -13,7 +13,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
import io.element.android.features.analytics.api.AnalyticsOptInEvent
|
||||||
import io.element.android.features.analytics.api.R
|
import io.element.android.features.analytics.api.R
|
||||||
import io.element.android.libraries.designsystem.components.LINK_TAG
|
import io.element.android.libraries.designsystem.components.LINK_TAG
|
||||||
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
@@ -30,7 +30,7 @@ fun AnalyticsPreferencesView(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
fun onEnabledChanged(isEnabled: Boolean) {
|
fun onEnabledChanged(isEnabled: Boolean) {
|
||||||
state.eventSink(AnalyticsOptInEvents.EnableAnalytics(isEnabled = isEnabled))
|
state.eventSink(AnalyticsOptInEvent.EnableAnalytics(isEnabled = isEnabled))
|
||||||
}
|
}
|
||||||
|
|
||||||
val supportingText = stringResource(
|
val supportingText = stringResource(
|
||||||
@@ -66,7 +66,7 @@ fun AnalyticsPreferencesView(
|
|||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AnalyticsPreferencesViewPreview(@PreviewParameter(AnalyticsPreferencesStateProvider::class) state: AnalyticsPreferencesState) =
|
internal fun AnalyticsPreferencesViewPreview(@PreviewParameter(AnalyticsPreferencesStatePreviewParam::class) state: AnalyticsPreferencesState) =
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
AnalyticsPreferencesView(
|
AnalyticsPreferencesView(
|
||||||
state = state,
|
state = state,
|
||||||
|
|||||||
+3
-3
@@ -12,7 +12,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.appconfig.AnalyticsConfig
|
import io.element.android.appconfig.AnalyticsConfig
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
import io.element.android.features.analytics.api.AnalyticsOptInEvent
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
@@ -28,9 +28,9 @@ class AnalyticsOptInPresenter(
|
|||||||
override fun present(): AnalyticsOptInState {
|
override fun present(): AnalyticsOptInState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
fun handleEvent(event: AnalyticsOptInEvents) {
|
fun handleEvent(event: AnalyticsOptInEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is AnalyticsOptInEvents.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
|
is AnalyticsOptInEvent.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
|
||||||
}
|
}
|
||||||
localCoroutineScope.launch {
|
localCoroutineScope.launch {
|
||||||
analyticsService.setDidAskUserConsent()
|
analyticsService.setDidAskUserConsent()
|
||||||
|
|||||||
+2
-2
@@ -8,10 +8,10 @@
|
|||||||
|
|
||||||
package io.element.android.features.analytics.impl
|
package io.element.android.features.analytics.impl
|
||||||
|
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
import io.element.android.features.analytics.api.AnalyticsOptInEvent
|
||||||
|
|
||||||
data class AnalyticsOptInState(
|
data class AnalyticsOptInState(
|
||||||
val applicationName: String,
|
val applicationName: String,
|
||||||
val hasPolicyLink: Boolean,
|
val hasPolicyLink: Boolean,
|
||||||
val eventSink: (AnalyticsOptInEvents) -> Unit
|
val eventSink: (AnalyticsOptInEvent) -> Unit
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ package io.element.android.features.analytics.impl
|
|||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
|
||||||
open class AnalyticsOptInStateProvider : PreviewParameterProvider<AnalyticsOptInState> {
|
open class AnalyticsOptInStatePreviewParam : PreviewParameterProvider<AnalyticsOptInState> {
|
||||||
override val values: Sequence<AnalyticsOptInState>
|
override val values: Sequence<AnalyticsOptInState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aAnalyticsOptInState(),
|
aAnalyticsOptInState(),
|
||||||
+4
-4
@@ -28,7 +28,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import io.element.android.appconfig.AnalyticsConfig
|
import io.element.android.appconfig.AnalyticsConfig
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
import io.element.android.features.analytics.api.AnalyticsOptInEvent
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
@@ -55,11 +55,11 @@ fun AnalyticsOptInView(
|
|||||||
val eventSink = state.eventSink
|
val eventSink = state.eventSink
|
||||||
|
|
||||||
fun onAcceptTerms() {
|
fun onAcceptTerms() {
|
||||||
eventSink(AnalyticsOptInEvents.EnableAnalytics(true))
|
eventSink(AnalyticsOptInEvent.EnableAnalytics(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDeclineTerms() {
|
fun onDeclineTerms() {
|
||||||
eventSink(AnalyticsOptInEvents.EnableAnalytics(false))
|
eventSink(AnalyticsOptInEvent.EnableAnalytics(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
BackHandler(onBack = ::onDeclineTerms)
|
BackHandler(onBack = ::onDeclineTerms)
|
||||||
@@ -171,7 +171,7 @@ private fun AnalyticsOptInFooter(
|
|||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AnalyticsOptInViewPreview(@PreviewParameter(AnalyticsOptInStateProvider::class) state: AnalyticsOptInState) = ElementPreview {
|
internal fun AnalyticsOptInViewPreview(@PreviewParameter(AnalyticsOptInStatePreviewParam::class) state: AnalyticsOptInState) = ElementPreview {
|
||||||
AnalyticsOptInView(
|
AnalyticsOptInView(
|
||||||
state = state,
|
state = state,
|
||||||
onClickTerms = {},
|
onClickTerms = {},
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ import io.element.android.libraries.architecture.Presenter
|
|||||||
|
|
||||||
@ContributesTo(AppScope::class)
|
@ContributesTo(AppScope::class)
|
||||||
@BindingContainer
|
@BindingContainer
|
||||||
interface AnalyticsModule {
|
interface AnalyticsBindingContainer {
|
||||||
@Binds
|
@Binds
|
||||||
fun bindAnalyticsPreferencesPresenter(presenter: AnalyticsPreferencesPresenter): Presenter<AnalyticsPreferencesState>
|
fun bindAnalyticsPreferencesPresenter(presenter: AnalyticsPreferencesPresenter): Presenter<AnalyticsPreferencesState>
|
||||||
}
|
}
|
||||||
+3
-3
@@ -13,7 +13,7 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.appconfig.AnalyticsConfig
|
import io.element.android.appconfig.AnalyticsConfig
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
import io.element.android.features.analytics.api.AnalyticsOptInEvent
|
||||||
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
|
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
@@ -31,9 +31,9 @@ class AnalyticsPreferencesPresenter(
|
|||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val isEnabled = analyticsService.userConsentFlow.collectAsState(initial = false)
|
val isEnabled = analyticsService.userConsentFlow.collectAsState(initial = false)
|
||||||
|
|
||||||
fun handleEvent(event: AnalyticsOptInEvents) {
|
fun handleEvent(event: AnalyticsOptInEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is AnalyticsOptInEvents.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
|
is AnalyticsOptInEvent.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -12,7 +12,7 @@ import app.cash.molecule.RecompositionMode
|
|||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
import io.element.android.features.analytics.api.AnalyticsOptInEvent
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
@@ -37,7 +37,7 @@ class AnalyticsOptInPresenterTest {
|
|||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(analyticsService.didAskUserConsentFlow.first()).isFalse()
|
assertThat(analyticsService.didAskUserConsentFlow.first()).isFalse()
|
||||||
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(true))
|
initialState.eventSink.invoke(AnalyticsOptInEvent.EnableAnalytics(true))
|
||||||
assertThat(analyticsService.didAskUserConsentFlow.first()).isTrue()
|
assertThat(analyticsService.didAskUserConsentFlow.first()).isTrue()
|
||||||
assertThat(analyticsService.userConsentFlow.first()).isTrue()
|
assertThat(analyticsService.userConsentFlow.first()).isTrue()
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ class AnalyticsOptInPresenterTest {
|
|||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(analyticsService.didAskUserConsentFlow.first()).isFalse()
|
assertThat(analyticsService.didAskUserConsentFlow.first()).isFalse()
|
||||||
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(false))
|
initialState.eventSink.invoke(AnalyticsOptInEvent.EnableAnalytics(false))
|
||||||
assertThat(analyticsService.didAskUserConsentFlow.first()).isTrue()
|
assertThat(analyticsService.didAskUserConsentFlow.first()).isTrue()
|
||||||
assertThat(analyticsService.userConsentFlow.first()).isFalse()
|
assertThat(analyticsService.userConsentFlow.first()).isFalse()
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -13,7 +13,7 @@ import app.cash.molecule.moleculeFlow
|
|||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.appconfig.AnalyticsConfig
|
import io.element.android.appconfig.AnalyticsConfig
|
||||||
import io.element.android.features.analytics.api.AnalyticsOptInEvents
|
import io.element.android.features.analytics.api.AnalyticsOptInEvent
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
@@ -67,9 +67,9 @@ class AnalyticsPreferencesPresenterTest {
|
|||||||
skipItems(1)
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.isEnabled).isTrue()
|
assertThat(initialState.isEnabled).isTrue()
|
||||||
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(false))
|
initialState.eventSink.invoke(AnalyticsOptInEvent.EnableAnalytics(false))
|
||||||
assertThat(awaitItem().isEnabled).isFalse()
|
assertThat(awaitItem().isEnabled).isFalse()
|
||||||
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(true))
|
initialState.eventSink.invoke(AnalyticsOptInEvent.EnableAnalytics(true))
|
||||||
assertThat(awaitItem().isEnabled).isTrue()
|
assertThat(awaitItem().isEnabled).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ package io.element.android.features.announcement.impl
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.announcement.api.Announcement
|
import io.element.android.features.announcement.api.Announcement
|
||||||
|
|
||||||
open class AnnouncementStateProvider : PreviewParameterProvider<AnnouncementState> {
|
open class AnnouncementStatePreviewParam : PreviewParameterProvider<AnnouncementState> {
|
||||||
override val values: Sequence<AnnouncementState>
|
override val values: Sequence<AnnouncementState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
anAnnouncementState(),
|
anAnnouncementState(),
|
||||||
+2
-2
@@ -33,7 +33,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
|||||||
import io.element.android.features.announcement.api.Announcement
|
import io.element.android.features.announcement.api.Announcement
|
||||||
import io.element.android.features.announcement.impl.AnnouncementEvent
|
import io.element.android.features.announcement.impl.AnnouncementEvent
|
||||||
import io.element.android.features.announcement.impl.AnnouncementState
|
import io.element.android.features.announcement.impl.AnnouncementState
|
||||||
import io.element.android.features.announcement.impl.AnnouncementStateProvider
|
import io.element.android.features.announcement.impl.AnnouncementStatePreviewParam
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
@@ -216,7 +216,7 @@ private fun Announcement.Fullscreen.notice(): String? = when (this) {
|
|||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun FullscreenAnnouncementViewPreview(@PreviewParameter(AnnouncementStateProvider::class) state: AnnouncementState) = ElementPreview {
|
internal fun FullscreenAnnouncementViewPreview(@PreviewParameter(AnnouncementStatePreviewParam::class) state: AnnouncementState) = ElementPreview {
|
||||||
FullscreenAnnouncementView(
|
FullscreenAnnouncementView(
|
||||||
state = state,
|
state = state,
|
||||||
)
|
)
|
||||||
|
|||||||
+2
@@ -11,7 +11,9 @@ package io.element.android.features.call.impl.pip
|
|||||||
import io.element.android.features.call.impl.utils.PipController
|
import io.element.android.features.call.impl.utils.PipController
|
||||||
|
|
||||||
sealed interface PictureInPictureEvent {
|
sealed interface PictureInPictureEvent {
|
||||||
|
data object OnCallStarted : PictureInPictureEvent
|
||||||
data class SetPipController(val pipController: PipController) : PictureInPictureEvent
|
data class SetPipController(val pipController: PipController) : PictureInPictureEvent
|
||||||
data object EnterPictureInPicture : PictureInPictureEvent
|
data object EnterPictureInPicture : PictureInPictureEvent
|
||||||
data class OnPictureInPictureModeChanged(val isInPip: Boolean) : PictureInPictureEvent
|
data class OnPictureInPictureModeChanged(val isInPip: Boolean) : PictureInPictureEvent
|
||||||
|
data class SetPipOrientation(val orientation: Int?) : PictureInPictureEvent
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -38,6 +38,9 @@ class PictureInPicturePresenter(
|
|||||||
|
|
||||||
fun handleEvent(event: PictureInPictureEvent) {
|
fun handleEvent(event: PictureInPictureEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
is PictureInPictureEvent.OnCallStarted -> {
|
||||||
|
pipController?.setupPipCallbacks()
|
||||||
|
}
|
||||||
is PictureInPictureEvent.SetPipController -> {
|
is PictureInPictureEvent.SetPipController -> {
|
||||||
pipController = event.pipController
|
pipController = event.pipController
|
||||||
}
|
}
|
||||||
@@ -55,6 +58,12 @@ class PictureInPicturePresenter(
|
|||||||
pipController?.exitPip()
|
pipController?.exitPip()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is PictureInPictureEvent.SetPipOrientation -> {
|
||||||
|
Timber.tag(loggerTag.value).d("onOrientationChange: ${event.orientation}")
|
||||||
|
if (isPipSupported) {
|
||||||
|
pipView?.setPipOrientation(event.orientation)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
@@ -8,8 +8,15 @@
|
|||||||
|
|
||||||
package io.element.android.features.call.impl.pip
|
package io.element.android.features.call.impl.pip
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
|
||||||
interface PipView {
|
interface PipView {
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
fun setPipOrientation(orientation: Int?)
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
fun setPipParams()
|
fun setPipParams()
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
fun enterPipMode(): Boolean
|
fun enterPipMode(): Boolean
|
||||||
fun hangUp()
|
fun hangUp()
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
||||||
open class CallNotificationDataProvider : PreviewParameterProvider<CallNotificationData> {
|
open class CallNotificationDataPreviewParam : PreviewParameterProvider<CallNotificationData> {
|
||||||
override val values: Sequence<CallNotificationData>
|
override val values: Sequence<CallNotificationData>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aCallNotificationData(
|
aCallNotificationData(
|
||||||
+1
-1
@@ -11,7 +11,7 @@ package io.element.android.features.call.impl.ui
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
|
||||||
open class CallScreenStateProvider : PreviewParameterProvider<CallScreenState> {
|
open class CallScreenStatePreviewParam : PreviewParameterProvider<CallScreenState> {
|
||||||
override val values: Sequence<CallScreenState>
|
override val values: Sequence<CallScreenState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aCallScreenState(),
|
aCallScreenState(),
|
||||||
+8
-2
@@ -118,6 +118,7 @@ internal fun CallScreenView(
|
|||||||
if (webViewAudioManager?.isInCallMode?.get() == false) {
|
if (webViewAudioManager?.isInCallMode?.get() == false) {
|
||||||
Timber.d("URL $url is loaded, starting in-call audio mode")
|
Timber.d("URL $url is loaded, starting in-call audio mode")
|
||||||
webViewAudioManager?.onCallStarted()
|
webViewAudioManager?.onCallStarted()
|
||||||
|
pipState.eventSink(PictureInPictureEvent.OnCallStarted)
|
||||||
} else {
|
} else {
|
||||||
Timber.d("Can't start in-call audio mode since the app is already in it.")
|
Timber.d("Can't start in-call audio mode since the app is already in it.")
|
||||||
}
|
}
|
||||||
@@ -130,7 +131,12 @@ internal fun CallScreenView(
|
|||||||
onInvalidAudioDeviceAdded = { invalidAudioDeviceReason = it },
|
onInvalidAudioDeviceAdded = { invalidAudioDeviceReason = it },
|
||||||
)
|
)
|
||||||
state.eventSink(CallScreenEvent.SetupMessageChannels(interceptor))
|
state.eventSink(CallScreenEvent.SetupMessageChannels(interceptor))
|
||||||
val pipController = WebViewPipController(webView)
|
val pipController = WebViewPipController(
|
||||||
|
webView = webView,
|
||||||
|
updatePipOrientation = { orientation ->
|
||||||
|
pipState.eventSink(PictureInPictureEvent.SetPipOrientation(orientation))
|
||||||
|
}
|
||||||
|
)
|
||||||
pipState.eventSink(PictureInPictureEvent.SetPipController(pipController))
|
pipState.eventSink(PictureInPictureEvent.SetPipController(pipController))
|
||||||
},
|
},
|
||||||
onDestroyWebView = {
|
onDestroyWebView = {
|
||||||
@@ -260,7 +266,7 @@ private fun WebView.dispatchEscKeyEvent() {
|
|||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun CallScreenViewPreview(
|
internal fun CallScreenViewPreview(
|
||||||
@PreviewParameter(CallScreenStateProvider::class) state: CallScreenState,
|
@PreviewParameter(CallScreenStatePreviewParam::class) state: CallScreenState,
|
||||||
) = ElementPreview {
|
) = ElementPreview {
|
||||||
CallScreenView(
|
CallScreenView(
|
||||||
state = state,
|
state = state,
|
||||||
|
|||||||
+16
-3
@@ -11,9 +11,9 @@ package io.element.android.features.call.impl.ui
|
|||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.PictureInPictureParams
|
import android.app.PictureInPictureParams
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Rational
|
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.webkit.PermissionRequest
|
import android.webkit.PermissionRequest
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@@ -49,6 +49,7 @@ import io.element.android.features.call.impl.pip.PipView
|
|||||||
import io.element.android.features.call.impl.services.CallForegroundService
|
import io.element.android.features.call.impl.services.CallForegroundService
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.libraries.androidutils.browser.ConsoleMessageLogger
|
import io.element.android.libraries.androidutils.browser.ConsoleMessageLogger
|
||||||
|
import io.element.android.libraries.androidutils.media.setAspectRatioFromOrientation
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.bindings
|
import io.element.android.libraries.architecture.bindings
|
||||||
import io.element.android.libraries.audio.api.AudioFocus
|
import io.element.android.libraries.audio.api.AudioFocus
|
||||||
@@ -86,6 +87,8 @@ class ElementCallActivity :
|
|||||||
|
|
||||||
private var eventSink: ((CallScreenEvent) -> Unit)? = null
|
private var eventSink: ((CallScreenEvent) -> Unit)? = null
|
||||||
|
|
||||||
|
private var currentPipOrientation: Int? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -199,6 +202,9 @@ class ElementCallActivity :
|
|||||||
}
|
}
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
val onPictureInPictureModeChangedListener = Consumer { _: PictureInPictureModeChangedInfo ->
|
val onPictureInPictureModeChangedListener = Consumer { _: PictureInPictureModeChangedInfo ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
setPipParams()
|
||||||
|
}
|
||||||
pipEventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(isInPictureInPictureMode))
|
pipEventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(isInPictureInPictureMode))
|
||||||
if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
||||||
Timber.tag(loggerTag.value).d("Exiting PiP mode: Hangup the call")
|
Timber.tag(loggerTag.value).d("Exiting PiP mode: Hangup the call")
|
||||||
@@ -296,11 +302,18 @@ class ElementCallActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
override fun setPipOrientation(orientation: Int?) {
|
||||||
|
currentPipOrientation = orientation
|
||||||
|
setPictureInPictureParams(getPictureInPictureParams())
|
||||||
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
private fun getPictureInPictureParams(): PictureInPictureParams {
|
private fun getPictureInPictureParams(): PictureInPictureParams {
|
||||||
|
// Portrait for calls seems more appropriate as a fallback value
|
||||||
|
val orientation = currentPipOrientation ?: Configuration.ORIENTATION_PORTRAIT
|
||||||
return PictureInPictureParams.Builder()
|
return PictureInPictureParams.Builder()
|
||||||
// Portrait for calls seems more appropriate
|
.setAspectRatioFromOrientation(orientation)
|
||||||
.setAspectRatio(Rational(3, 5))
|
|
||||||
.apply {
|
.apply {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
setAutoEnterEnabled(true)
|
setAutoEnterEnabled(true)
|
||||||
|
|||||||
+1
-1
@@ -161,7 +161,7 @@ private fun ActionButton(
|
|||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun IncomingCallScreenPreview(
|
internal fun IncomingCallScreenPreview(
|
||||||
@PreviewParameter(CallNotificationDataProvider::class) state: CallNotificationData,
|
@PreviewParameter(CallNotificationDataPreviewParam::class) state: CallNotificationData,
|
||||||
) = ElementPreview {
|
) = ElementPreview {
|
||||||
IncomingCallScreen(
|
IncomingCallScreen(
|
||||||
notificationData = state,
|
notificationData = state,
|
||||||
|
|||||||
+1
@@ -9,6 +9,7 @@
|
|||||||
package io.element.android.features.call.impl.utils
|
package io.element.android.features.call.impl.utils
|
||||||
|
|
||||||
interface PipController {
|
interface PipController {
|
||||||
|
fun setupPipCallbacks()
|
||||||
suspend fun canEnterPip(): Boolean
|
suspend fun canEnterPip(): Boolean
|
||||||
fun enterPip()
|
fun enterPip()
|
||||||
fun exitPip()
|
fun exitPip()
|
||||||
|
|||||||
+1
-1
@@ -250,7 +250,7 @@ class WebViewAudioManager(
|
|||||||
|
|
||||||
hasRegisteredCallbacks = true
|
hasRegisteredCallbacks = true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
Timber.d("Setting androidNativeBridge javascript interface in webview")
|
Timber.d("Setting androidNativeBridge javascript interface in webview")
|
||||||
webView.addJavascriptInterface(webViewAudioDeviceSelectedCallback, "androidNativeBridge")
|
webView.addJavascriptInterface(webViewAudioDeviceSelectedCallback, "androidNativeBridge")
|
||||||
|
|||||||
+30
@@ -8,7 +8,10 @@
|
|||||||
|
|
||||||
package io.element.android.features.call.impl.utils
|
package io.element.android.features.call.impl.utils
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.webkit.JavascriptInterface
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import timber.log.Timber
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
@@ -18,7 +21,18 @@ import kotlin.coroutines.suspendCoroutine
|
|||||||
*/
|
*/
|
||||||
class WebViewPipController(
|
class WebViewPipController(
|
||||||
private val webView: WebView,
|
private val webView: WebView,
|
||||||
|
updatePipOrientation: (orientation: Int?) -> Unit,
|
||||||
) : PipController {
|
) : PipController {
|
||||||
|
init {
|
||||||
|
// Register the JavaScript interface ahead of time to ensure it's available when the WebView content is loaded and tries to call it
|
||||||
|
webView.addJavascriptInterface(PipBridge(updatePipOrientation), "androidPipBridge")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setupPipCallbacks() {
|
||||||
|
Timber.d("Adding callback in controls.onPipMediaOrientationUpdate")
|
||||||
|
webView.evaluateJavascript("controls.onPipMediaOrientationUpdate = (orientation) => { androidPipBridge.setPipOrientation(orientation); };", null)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun canEnterPip(): Boolean {
|
override suspend fun canEnterPip(): Boolean {
|
||||||
return suspendCoroutine { continuation ->
|
return suspendCoroutine { continuation ->
|
||||||
webView.evaluateJavascript("controls.canEnterPip()") { result ->
|
webView.evaluateJavascript("controls.canEnterPip()") { result ->
|
||||||
@@ -36,3 +50,19 @@ class WebViewPipController(
|
|||||||
webView.evaluateJavascript("controls.disablePip()", null)
|
webView.evaluateJavascript("controls.disablePip()", null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class PipBridge(
|
||||||
|
private val onOrientationChange: (orientation: Int?) -> Unit,
|
||||||
|
) {
|
||||||
|
@JavascriptInterface
|
||||||
|
fun setPipOrientation(orientation: String?) {
|
||||||
|
Timber.d("Picture-in-picture orientation changed in webview, orientation: $orientation")
|
||||||
|
// This callback is used to update the picture-in-picture orientation in the WebView when it changes
|
||||||
|
val orientation = when (orientation) {
|
||||||
|
"portrait" -> Configuration.ORIENTATION_PORTRAIT
|
||||||
|
"landscape" -> Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
onOrientationChange(orientation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+3
@@ -12,10 +12,13 @@ import io.element.android.features.call.impl.utils.PipController
|
|||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
|
||||||
class FakePipController(
|
class FakePipController(
|
||||||
|
private val setupPipCallbacksResult: () -> Unit = { lambdaError() },
|
||||||
private val canEnterPipResult: () -> Boolean = { lambdaError() },
|
private val canEnterPipResult: () -> Boolean = { lambdaError() },
|
||||||
private val enterPipResult: () -> Unit = { lambdaError() },
|
private val enterPipResult: () -> Unit = { lambdaError() },
|
||||||
private val exitPipResult: () -> Unit = { lambdaError() },
|
private val exitPipResult: () -> Unit = { lambdaError() },
|
||||||
) : PipController {
|
) : PipController {
|
||||||
|
override fun setupPipCallbacks() = setupPipCallbacksResult()
|
||||||
|
|
||||||
override suspend fun canEnterPip(): Boolean = canEnterPipResult()
|
override suspend fun canEnterPip(): Boolean = canEnterPipResult()
|
||||||
|
|
||||||
override fun enterPip() = enterPipResult()
|
override fun enterPip() = enterPipResult()
|
||||||
|
|||||||
+3
-1
@@ -13,9 +13,11 @@ import io.element.android.tests.testutils.lambda.lambdaError
|
|||||||
class FakePipView(
|
class FakePipView(
|
||||||
private val setPipParamsResult: () -> Unit = { lambdaError() },
|
private val setPipParamsResult: () -> Unit = { lambdaError() },
|
||||||
private val enterPipModeResult: () -> Boolean = { lambdaError() },
|
private val enterPipModeResult: () -> Boolean = { lambdaError() },
|
||||||
private val handUpResult: () -> Unit = { lambdaError() }
|
private val handUpResult: () -> Unit = { lambdaError() },
|
||||||
|
private val onOrientationChangeResult: (orientation: Int?) -> Unit = { lambdaError() },
|
||||||
) : PipView {
|
) : PipView {
|
||||||
override fun setPipParams() = setPipParamsResult()
|
override fun setPipParams() = setPipParamsResult()
|
||||||
override fun enterPipMode(): Boolean = enterPipModeResult()
|
override fun enterPipMode(): Boolean = enterPipModeResult()
|
||||||
override fun hangUp() = handUpResult()
|
override fun hangUp() = handUpResult()
|
||||||
|
override fun setPipOrientation(orientation: Int?) = onOrientationChangeResult(orientation)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
|
|
||||||
@BindingContainer
|
@BindingContainer
|
||||||
@ContributesTo(RoomScope::class)
|
@ContributesTo(RoomScope::class)
|
||||||
object ContentScannerModule {
|
object ContentScannerBindingContainer {
|
||||||
@Provides
|
@Provides
|
||||||
@SingleIn(RoomScope::class)
|
@SingleIn(RoomScope::class)
|
||||||
fun providesContentScannerService(
|
fun providesContentScannerService(
|
||||||
+5
-5
@@ -15,16 +15,16 @@ import io.element.android.tests.testutils.testCoroutineDispatchers
|
|||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class ContentScannerModuleTest {
|
class ContentScannerBindingContainerTest {
|
||||||
@Test
|
@Test
|
||||||
fun `ContentScannerModule provides NoopContentScannerService when ContentScanner is null`() = runTest {
|
fun `ContentScannerBindingContainer provides NoopContentScannerService when ContentScanner is null`() = runTest {
|
||||||
// Given
|
// Given
|
||||||
val contentScanner: ContentScanner? = null
|
val contentScanner: ContentScanner? = null
|
||||||
val sessionCoroutineScope = backgroundScope
|
val sessionCoroutineScope = backgroundScope
|
||||||
val coroutineDispatchers = testCoroutineDispatchers()
|
val coroutineDispatchers = testCoroutineDispatchers()
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val contentScannerService = ContentScannerModule.providesContentScannerService(
|
val contentScannerService = ContentScannerBindingContainer.providesContentScannerService(
|
||||||
contentScanner = contentScanner,
|
contentScanner = contentScanner,
|
||||||
coroutineScope = sessionCoroutineScope,
|
coroutineScope = sessionCoroutineScope,
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
@@ -35,14 +35,14 @@ class ContentScannerModuleTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ContentScannerModule provides DefaultContentScannerService when ContentScanner is not null`() = runTest {
|
fun `ContentScannerBindingContainer provides DefaultContentScannerService when ContentScanner is not null`() = runTest {
|
||||||
// Given
|
// Given
|
||||||
val contentScanner: ContentScanner? = FakeContentScanner()
|
val contentScanner: ContentScanner? = FakeContentScanner()
|
||||||
val sessionCoroutineScope = backgroundScope
|
val sessionCoroutineScope = backgroundScope
|
||||||
val coroutineDispatchers = testCoroutineDispatchers()
|
val coroutineDispatchers = testCoroutineDispatchers()
|
||||||
|
|
||||||
// When
|
// When
|
||||||
val contentScannerService = ContentScannerModule.providesContentScannerService(
|
val contentScannerService = ContentScannerBindingContainer.providesContentScannerService(
|
||||||
contentScanner = contentScanner,
|
contentScanner = contentScanner,
|
||||||
coroutineScope = sessionCoroutineScope,
|
coroutineScope = sessionCoroutineScope,
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
+4
-4
@@ -21,9 +21,9 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.createroom.impl.R
|
import io.element.android.features.createroom.impl.R
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
import io.element.android.features.invitepeople.api.InvitePeopleEvent
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleState
|
import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleStateProvider
|
import io.element.android.features.invitepeople.api.InvitePeopleStatePreviewParam
|
||||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
@@ -56,7 +56,7 @@ fun AddPeopleView(
|
|||||||
Button(
|
Button(
|
||||||
text = stringResource(CommonStrings.action_finish),
|
text = stringResource(CommonStrings.action_finish),
|
||||||
onClick = {
|
onClick = {
|
||||||
state.eventSink(InvitePeopleEvents.SendInvites)
|
state.eventSink(InvitePeopleEvent.SendInvites)
|
||||||
},
|
},
|
||||||
enabled = state.canInvite,
|
enabled = state.canInvite,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -86,7 +86,7 @@ private fun AddPeopleTopBar(
|
|||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AddPeopleViewPreview(@PreviewParameter(InvitePeopleStateProvider::class) state: InvitePeopleState) = ElementPreview {
|
internal fun AddPeopleViewPreview(@PreviewParameter(InvitePeopleStatePreviewParam::class) state: InvitePeopleState) = ElementPreview {
|
||||||
AddPeopleView(
|
AddPeopleView(
|
||||||
state = state,
|
state = state,
|
||||||
invitePeopleView = {},
|
invitePeopleView = {},
|
||||||
|
|||||||
+9
-9
@@ -11,13 +11,13 @@ package io.element.android.features.createroom.impl.configureroom
|
|||||||
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
|
||||||
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
import io.element.android.libraries.matrix.ui.media.AvatarAction
|
||||||
|
|
||||||
sealed interface ConfigureRoomEvents {
|
sealed interface ConfigureRoomEvent {
|
||||||
data class RoomNameChanged(val name: String) : ConfigureRoomEvents
|
data class RoomNameChanged(val name: String) : ConfigureRoomEvent
|
||||||
data class TopicChanged(val topic: String) : ConfigureRoomEvents
|
data class TopicChanged(val topic: String) : ConfigureRoomEvent
|
||||||
data class JoinRuleChanged(val joinRuleItem: JoinRuleItem) : ConfigureRoomEvents
|
data class JoinRuleChanged(val joinRuleItem: JoinRuleItem) : ConfigureRoomEvent
|
||||||
data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvents
|
data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvent
|
||||||
data object CreateRoom : ConfigureRoomEvents
|
data object CreateRoom : ConfigureRoomEvent
|
||||||
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
|
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvent
|
||||||
data class SetParentSpace(val space: SpaceRoom?) : ConfigureRoomEvents
|
data class SetParentSpace(val space: SpaceRoom?) : ConfigureRoomEvent
|
||||||
data object CancelCreateRoom : ConfigureRoomEvents
|
data object CancelCreateRoom : ConfigureRoomEvent
|
||||||
}
|
}
|
||||||
+9
-9
@@ -188,14 +188,14 @@ class ConfigureRoomPresenter(
|
|||||||
localCoroutineScope.createRoom(config, createRoomAction)
|
localCoroutineScope.createRoom(config, createRoomAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleEvent(event: ConfigureRoomEvents) {
|
fun handleEvent(event: ConfigureRoomEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
|
is ConfigureRoomEvent.RoomNameChanged -> dataStore.setRoomName(event.name)
|
||||||
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
|
is ConfigureRoomEvent.TopicChanged -> dataStore.setTopic(event.topic)
|
||||||
is ConfigureRoomEvents.JoinRuleChanged -> dataStore.setJoinRule(event.joinRuleItem)
|
is ConfigureRoomEvent.JoinRuleChanged -> dataStore.setJoinRule(event.joinRuleItem)
|
||||||
is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
|
is ConfigureRoomEvent.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
|
||||||
is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig)
|
is ConfigureRoomEvent.CreateRoom -> createRoom(createRoomConfig)
|
||||||
is ConfigureRoomEvents.HandleAvatarAction -> {
|
is ConfigureRoomEvent.HandleAvatarAction -> {
|
||||||
when (event.action) {
|
when (event.action) {
|
||||||
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
|
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
|
||||||
AvatarAction.TakePhoto -> if (cameraPermissionState.permissionGranted) {
|
AvatarAction.TakePhoto -> if (cameraPermissionState.permissionGranted) {
|
||||||
@@ -207,10 +207,10 @@ class ConfigureRoomPresenter(
|
|||||||
AvatarAction.Remove -> dataStore.setAvatarUri(uri = null)
|
AvatarAction.Remove -> dataStore.setAvatarUri(uri = null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is ConfigureRoomEvents.SetParentSpace -> {
|
is ConfigureRoomEvent.SetParentSpace -> {
|
||||||
dataStore.setParentSpace(event.space, false)
|
dataStore.setParentSpace(event.space, false)
|
||||||
}
|
}
|
||||||
ConfigureRoomEvents.CancelCreateRoom -> {
|
ConfigureRoomEvent.CancelCreateRoom -> {
|
||||||
createRoomAction.value = AsyncAction.Uninitialized
|
createRoomAction.value = AsyncAction.Uninitialized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ data class ConfigureRoomState(
|
|||||||
val homeserverName: String,
|
val homeserverName: String,
|
||||||
val availableJoinRules: ImmutableList<JoinRuleItem>,
|
val availableJoinRules: ImmutableList<JoinRuleItem>,
|
||||||
val spaces: ImmutableList<SpaceRoom>,
|
val spaces: ImmutableList<SpaceRoom>,
|
||||||
val eventSink: (ConfigureRoomEvents) -> Unit
|
val eventSink: (ConfigureRoomEvent) -> Unit
|
||||||
) {
|
) {
|
||||||
val isValid: Boolean = config.roomName?.isNotEmpty() == true &&
|
val isValid: Boolean = config.roomName?.isNotEmpty() == true &&
|
||||||
(config.visibilityState is RoomVisibilityState.Private || roomAddressValidity == RoomAddressValidity.Valid) &&
|
(config.visibilityState is RoomVisibilityState.Private || roomAddressValidity == RoomAddressValidity.Valid) &&
|
||||||
|
|||||||
+2
-2
@@ -21,7 +21,7 @@ import io.element.android.libraries.permissions.api.aPermissionsState
|
|||||||
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
import io.element.android.libraries.previewutils.room.aSpaceRoom
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomState> {
|
open class ConfigureRoomStatePreviewParam : PreviewParameterProvider<ConfigureRoomState> {
|
||||||
override val values: Sequence<ConfigureRoomState>
|
override val values: Sequence<ConfigureRoomState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aConfigureRoomState(),
|
aConfigureRoomState(),
|
||||||
@@ -143,7 +143,7 @@ fun aConfigureRoomState(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
spaces: List<SpaceRoom> = emptyList(),
|
spaces: List<SpaceRoom> = emptyList(),
|
||||||
eventSink: (ConfigureRoomEvents) -> Unit = { },
|
eventSink: (ConfigureRoomEvent) -> Unit = { },
|
||||||
) = ConfigureRoomState(
|
) = ConfigureRoomState(
|
||||||
config = config,
|
config = config,
|
||||||
isSpace = isSpace,
|
isSpace = isSpace,
|
||||||
+11
-11
@@ -96,7 +96,7 @@ fun ConfigureRoomView(
|
|||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
onNextClick = {
|
onNextClick = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
state.eventSink(ConfigureRoomEvents.CreateRoom)
|
state.eventSink(ConfigureRoomEvent.CreateRoom)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -114,20 +114,20 @@ fun ConfigureRoomView(
|
|||||||
avatarUri = state.config.avatarUri,
|
avatarUri = state.config.avatarUri,
|
||||||
roomName = state.config.roomName.orEmpty(),
|
roomName = state.config.roomName.orEmpty(),
|
||||||
onAvatarClick = ::onAvatarClick,
|
onAvatarClick = ::onAvatarClick,
|
||||||
onChangeRoomName = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
|
onChangeRoomName = { state.eventSink(ConfigureRoomEvent.RoomNameChanged(it)) },
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
RoomTopic(
|
RoomTopic(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
topic = state.config.topic.orEmpty(),
|
topic = state.config.topic.orEmpty(),
|
||||||
onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
|
onTopicChange = { state.eventSink(ConfigureRoomEvent.TopicChanged(it)) },
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
if (!state.isSpace && state.spaces.isNotEmpty()) {
|
if (!state.isSpace && state.spaces.isNotEmpty()) {
|
||||||
SelectParentSpaceOptions(
|
SelectParentSpaceOptions(
|
||||||
spaces = state.spaces,
|
spaces = state.spaces,
|
||||||
selectedSpace = state.config.parentSpace,
|
selectedSpace = state.config.parentSpace,
|
||||||
onSelectSpace = { state.eventSink(ConfigureRoomEvents.SetParentSpace(it)) },
|
onSelectSpace = { state.eventSink(ConfigureRoomEvent.SetParentSpace(it)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
RoomJoinRuleOptions(
|
RoomJoinRuleOptions(
|
||||||
@@ -136,7 +136,7 @@ fun ConfigureRoomView(
|
|||||||
parentSpace = state.config.parentSpace,
|
parentSpace = state.config.parentSpace,
|
||||||
onOptionClick = {
|
onOptionClick = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
state.eventSink(ConfigureRoomEvents.JoinRuleChanged(it))
|
state.eventSink(ConfigureRoomEvent.JoinRuleChanged(it))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (state.config.visibilityState !is RoomVisibilityState.Private) {
|
if (state.config.visibilityState !is RoomVisibilityState.Private) {
|
||||||
@@ -146,7 +146,7 @@ fun ConfigureRoomView(
|
|||||||
address = state.config.visibilityState.roomAddress().getOrNull().orEmpty(),
|
address = state.config.visibilityState.roomAddress().getOrNull().orEmpty(),
|
||||||
homeserverName = state.homeserverName,
|
homeserverName = state.homeserverName,
|
||||||
addressValidity = state.roomAddressValidity,
|
addressValidity = state.roomAddressValidity,
|
||||||
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
|
onAddressChange = { state.eventSink(ConfigureRoomEvent.RoomAddressChanged(it)) },
|
||||||
label = null,
|
label = null,
|
||||||
supportingText = stringResource(R.string.screen_create_room_room_address_section_footer),
|
supportingText = stringResource(R.string.screen_create_room_room_address_section_footer),
|
||||||
)
|
)
|
||||||
@@ -158,7 +158,7 @@ fun ConfigureRoomView(
|
|||||||
actions = state.avatarActions,
|
actions = state.avatarActions,
|
||||||
isVisible = isAvatarActionsSheetVisible.value,
|
isVisible = isAvatarActionsSheetVisible.value,
|
||||||
onDismiss = { isAvatarActionsSheetVisible.value = false },
|
onDismiss = { isAvatarActionsSheetVisible.value = false },
|
||||||
onSelectAction = { state.eventSink(ConfigureRoomEvents.HandleAvatarAction(it)) }
|
onSelectAction = { state.eventSink(ConfigureRoomEvent.HandleAvatarAction(it)) }
|
||||||
)
|
)
|
||||||
|
|
||||||
AsyncActionView(
|
AsyncActionView(
|
||||||
@@ -170,8 +170,8 @@ fun ConfigureRoomView(
|
|||||||
},
|
},
|
||||||
onSuccess = { onCreateRoomSuccess(it) },
|
onSuccess = { onCreateRoomSuccess(it) },
|
||||||
errorMessage = { stringResource(if (isSpace) R.string.screen_create_room_error_creating_space else R.string.screen_create_room_error_creating_room) },
|
errorMessage = { stringResource(if (isSpace) R.string.screen_create_room_error_creating_space else R.string.screen_create_room_error_creating_room) },
|
||||||
onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom) },
|
onRetry = { state.eventSink(ConfigureRoomEvent.CreateRoom) },
|
||||||
onErrorDismiss = { state.eventSink(ConfigureRoomEvents.CancelCreateRoom) },
|
onErrorDismiss = { state.eventSink(ConfigureRoomEvent.CancelCreateRoom) },
|
||||||
)
|
)
|
||||||
|
|
||||||
PermissionsView(
|
PermissionsView(
|
||||||
@@ -349,12 +349,12 @@ private fun RoomJoinRuleOptions(
|
|||||||
|
|
||||||
@PreviewWithLargeHeight
|
@PreviewWithLargeHeight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) =
|
internal fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStatePreviewParam::class) state: ConfigureRoomState) =
|
||||||
ElementPreviewLight { ContentToPreview(state) }
|
ElementPreviewLight { ContentToPreview(state) }
|
||||||
|
|
||||||
@PreviewWithLargeHeight
|
@PreviewWithLargeHeight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ConfigureRoomViewDarkPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) =
|
internal fun ConfigureRoomViewDarkPreview(@PreviewParameter(ConfigureRoomStatePreviewParam::class) state: ConfigureRoomState) =
|
||||||
ElementPreviewDark { ContentToPreview(state) }
|
ElementPreviewDark { ContentToPreview(state) }
|
||||||
|
|
||||||
@ExcludeFromCoverage
|
@ExcludeFromCoverage
|
||||||
|
|||||||
+36
-36
@@ -12,7 +12,7 @@ import androidx.core.net.toUri
|
|||||||
import app.cash.turbine.TurbineTestContext
|
import app.cash.turbine.TurbineTestContext
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.CreatedRoom
|
import im.vector.app.features.analytics.plan.CreatedRoom
|
||||||
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomEvents
|
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomEvent
|
||||||
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomPresenter
|
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomPresenter
|
||||||
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomState
|
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomState
|
||||||
import io.element.android.features.createroom.impl.configureroom.CreateRoomConfig
|
import io.element.android.features.createroom.impl.configureroom.CreateRoomConfig
|
||||||
@@ -106,14 +106,14 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
assertThat(initialState.isValid).isFalse()
|
assertThat(initialState.isValid).isFalse()
|
||||||
|
|
||||||
// Room name not empty
|
// Room name not empty
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
|
initialState.eventSink(ConfigureRoomEvent.RoomNameChanged(A_ROOM_NAME))
|
||||||
var newState: ConfigureRoomState = awaitItem()
|
var newState: ConfigureRoomState = awaitItem()
|
||||||
config = config.copy(roomName = A_ROOM_NAME)
|
config = config.copy(roomName = A_ROOM_NAME)
|
||||||
assertThat(newState.config).isEqualTo(config)
|
assertThat(newState.config).isEqualTo(config)
|
||||||
assertThat(newState.isValid).isTrue()
|
assertThat(newState.isValid).isTrue()
|
||||||
|
|
||||||
// Clear room name
|
// Clear room name
|
||||||
newState.eventSink(ConfigureRoomEvents.RoomNameChanged(""))
|
newState.eventSink(ConfigureRoomEvent.RoomNameChanged(""))
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
config = config.copy(roomName = null)
|
config = config.copy(roomName = null)
|
||||||
assertThat(newState.config).isEqualTo(config)
|
assertThat(newState.config).isEqualTo(config)
|
||||||
@@ -136,13 +136,13 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
var expectedConfig = CreateRoomConfig()
|
var expectedConfig = CreateRoomConfig()
|
||||||
assertThat(initialState.config).isEqualTo(expectedConfig)
|
assertThat(initialState.config).isEqualTo(expectedConfig)
|
||||||
// Room name
|
// Room name
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
|
initialState.eventSink(ConfigureRoomEvent.RoomNameChanged(A_ROOM_NAME))
|
||||||
var newState = awaitItem()
|
var newState = awaitItem()
|
||||||
expectedConfig = expectedConfig.copy(roomName = A_ROOM_NAME)
|
expectedConfig = expectedConfig.copy(roomName = A_ROOM_NAME)
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
|
||||||
// Room topic
|
// Room topic
|
||||||
newState.eventSink(ConfigureRoomEvents.TopicChanged(A_MESSAGE))
|
newState.eventSink(ConfigureRoomEvent.TopicChanged(A_MESSAGE))
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
expectedConfig = expectedConfig.copy(topic = A_MESSAGE)
|
expectedConfig = expectedConfig.copy(topic = A_MESSAGE)
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
@@ -153,7 +153,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
// From gallery
|
// From gallery
|
||||||
val uriFromGallery = AN_URI_FROM_GALLERY
|
val uriFromGallery = AN_URI_FROM_GALLERY
|
||||||
pickerProvider.givenResult(uriFromGallery.toUri())
|
pickerProvider.givenResult(uriFromGallery.toUri())
|
||||||
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
newState.eventSink(ConfigureRoomEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery)
|
expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery)
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
@@ -161,7 +161,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
val uriFromCamera = AN_URI_FROM_CAMERA
|
val uriFromCamera = AN_URI_FROM_CAMERA
|
||||||
pickerProvider.givenResult(uriFromCamera.toUri())
|
pickerProvider.givenResult(uriFromCamera.toUri())
|
||||||
assertThat(newState.cameraPermissionState.permissionGranted).isFalse()
|
assertThat(newState.cameraPermissionState.permissionGranted).isFalse()
|
||||||
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
|
newState.eventSink(ConfigureRoomEvent.HandleAvatarAction(AvatarAction.TakePhoto))
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
assertThat(newState.cameraPermissionState.showDialog).isTrue()
|
assertThat(newState.cameraPermissionState.showDialog).isTrue()
|
||||||
permissionsPresenter.setPermissionGranted()
|
permissionsPresenter.setPermissionGranted()
|
||||||
@@ -173,18 +173,18 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
// Do it again, no permission is requested
|
// Do it again, no permission is requested
|
||||||
val uriFromCamera2 = AN_URI_FROM_CAMERA_2
|
val uriFromCamera2 = AN_URI_FROM_CAMERA_2
|
||||||
pickerProvider.givenResult(uriFromCamera2.toUri())
|
pickerProvider.givenResult(uriFromCamera2.toUri())
|
||||||
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
|
newState.eventSink(ConfigureRoomEvent.HandleAvatarAction(AvatarAction.TakePhoto))
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera2)
|
expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera2)
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
// Remove
|
// Remove
|
||||||
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.Remove))
|
newState.eventSink(ConfigureRoomEvent.HandleAvatarAction(AvatarAction.Remove))
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
expectedConfig = expectedConfig.copy(avatarUri = null)
|
expectedConfig = expectedConfig.copy(avatarUri = null)
|
||||||
assertThat(newState.config).isEqualTo(expectedConfig)
|
assertThat(newState.config).isEqualTo(expectedConfig)
|
||||||
|
|
||||||
// Room privacy
|
// Room privacy
|
||||||
newState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
newState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||||
newState = awaitItem()
|
newState = awaitItem()
|
||||||
expectedConfig = expectedConfig.copy(
|
expectedConfig = expectedConfig.copy(
|
||||||
visibilityState = RoomVisibilityState.Public(
|
visibilityState = RoomVisibilityState.Public(
|
||||||
@@ -208,7 +208,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
|
|
||||||
matrixClient.givenCreateRoomResult(createRoomResult)
|
matrixClient.givenCreateRoomResult(createRoomResult)
|
||||||
|
|
||||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
|
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
val stateAfterCreateRoom = awaitItem()
|
val stateAfterCreateRoom = awaitItem()
|
||||||
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
|
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
@@ -239,13 +239,13 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
|
|
||||||
// Use a public parent space so AskToJoin is a valid option
|
// Use a public parent space so AskToJoin is a valid option
|
||||||
val parentSpace = aSpaceRoom(joinRule = JoinRule.Public)
|
val parentSpace = aSpaceRoom(joinRule = JoinRule.Public)
|
||||||
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace))
|
initialState.eventSink(ConfigureRoomEvent.SetParentSpace(parentSpace))
|
||||||
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
|
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
|
||||||
|
|
||||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.AskToJoin))
|
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.AskToJoin))
|
||||||
assertThat(awaitItem().config.visibilityState.joinRuleItem).isEqualTo(JoinRuleItem.PublicVisibility.AskToJoin)
|
assertThat(awaitItem().config.visibilityState.joinRuleItem).isEqualTo(JoinRuleItem.PublicVisibility.AskToJoin)
|
||||||
|
|
||||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
|
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
val stateAfterCreateRoom = awaitItem()
|
val stateAfterCreateRoom = awaitItem()
|
||||||
|
|
||||||
@@ -282,13 +282,13 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
|
|
||||||
// Use a public parent space so AskToJoin is a valid option
|
// Use a public parent space so AskToJoin is a valid option
|
||||||
val parentSpace = aSpaceRoom(joinRule = JoinRule.Public)
|
val parentSpace = aSpaceRoom(joinRule = JoinRule.Public)
|
||||||
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace))
|
initialState.eventSink(ConfigureRoomEvent.SetParentSpace(parentSpace))
|
||||||
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
|
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
|
||||||
|
|
||||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.AskToJoin))
|
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.AskToJoin))
|
||||||
assertThat(awaitItem().config.visibilityState.joinRuleItem).isEqualTo(JoinRuleItem.PublicVisibility.AskToJoin)
|
assertThat(awaitItem().config.visibilityState.joinRuleItem).isEqualTo(JoinRuleItem.PublicVisibility.AskToJoin)
|
||||||
|
|
||||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
|
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
|
||||||
|
|
||||||
// We immediately receive the room power levels info needed for adding the child to a space
|
// We immediately receive the room power levels info needed for adding the child to a space
|
||||||
val powerLevels = RoomPowerLevels(
|
val powerLevels = RoomPowerLevels(
|
||||||
@@ -339,7 +339,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
|
|
||||||
matrixClient.givenCreateRoomResult(createRoomResult)
|
matrixClient.givenCreateRoomResult(createRoomResult)
|
||||||
|
|
||||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
|
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
|
||||||
skipItems(2)
|
skipItems(2)
|
||||||
|
|
||||||
val analyticsEvent = analyticsService.capturedEvents.filterIsInstance<CreatedRoom>().firstOrNull()
|
val analyticsEvent = analyticsService.capturedEvents.filterIsInstance<CreatedRoom>().firstOrNull()
|
||||||
@@ -369,14 +369,14 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
mediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(file, mockk(), mockk())))
|
mediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(file, mockk(), mockk())))
|
||||||
matrixClient.givenUploadMediaResult(Result.failure(AN_EXCEPTION))
|
matrixClient.givenUploadMediaResult(Result.failure(AN_EXCEPTION))
|
||||||
|
|
||||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
|
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
val stateAfterCreateRoom = awaitItem()
|
val stateAfterCreateRoom = awaitItem()
|
||||||
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
|
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
assertThat(analyticsService.capturedEvents.filterIsInstance<CreatedRoom>()).isEmpty()
|
assertThat(analyticsService.capturedEvents.filterIsInstance<CreatedRoom>()).isEmpty()
|
||||||
|
|
||||||
matrixClient.givenUploadMediaResult(Result.success(AN_AVATAR_URL))
|
matrixClient.givenUploadMediaResult(Result.success(AN_AVATAR_URL))
|
||||||
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom)
|
stateAfterCreateRoom.eventSink(ConfigureRoomEvent.CreateRoom)
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
@@ -399,14 +399,14 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
fakeMatrixClient.givenCreateRoomResult(createRoomResult)
|
fakeMatrixClient.givenCreateRoomResult(createRoomResult)
|
||||||
|
|
||||||
// Create
|
// Create
|
||||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
|
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
val stateAfterCreateRoom = awaitItem()
|
val stateAfterCreateRoom = awaitItem()
|
||||||
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
|
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
assertThat((stateAfterCreateRoom.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
|
assertThat((stateAfterCreateRoom.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
|
||||||
|
|
||||||
// Retry
|
// Retry
|
||||||
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom)
|
stateAfterCreateRoom.eventSink(ConfigureRoomEvent.CreateRoom)
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
val stateAfterRetry = awaitItem()
|
val stateAfterRetry = awaitItem()
|
||||||
@@ -414,7 +414,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
assertThat((stateAfterRetry.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
|
assertThat((stateAfterRetry.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
|
||||||
|
|
||||||
// Cancel
|
// Cancel
|
||||||
stateAfterRetry.eventSink(ConfigureRoomEvents.CancelCreateRoom)
|
stateAfterRetry.eventSink(ConfigureRoomEvent.CancelCreateRoom)
|
||||||
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -430,9 +430,9 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = initialState()
|
val initialState = initialState()
|
||||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("invalid address"))
|
initialState.eventSink(ConfigureRoomEvent.RoomAddressChanged("invalid address"))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
@@ -450,9 +450,9 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = initialState()
|
val initialState = initialState()
|
||||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
|
initialState.eventSink(ConfigureRoomEvent.RoomAddressChanged("address"))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
@@ -470,9 +470,9 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = initialState()
|
val initialState = initialState()
|
||||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
|
initialState.eventSink(ConfigureRoomEvent.RoomAddressChanged("address"))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
@@ -488,22 +488,22 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
val initialState = initialState()
|
val initialState = initialState()
|
||||||
|
|
||||||
// First change the join rule to public
|
// First change the join rule to public
|
||||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||||
assertThat(awaitItem().config.visibilityState).isInstanceOf(RoomVisibilityState.Public::class.java)
|
assertThat(awaitItem().config.visibilityState).isInstanceOf(RoomVisibilityState.Public::class.java)
|
||||||
|
|
||||||
// Then check changing the parent space resets it to private
|
// Then check changing the parent space resets it to private
|
||||||
// (via LaunchedEffect fallback since Public is not in availableJoinRules for non-public parent)
|
// (via LaunchedEffect fallback since Public is not in availableJoinRules for non-public parent)
|
||||||
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(aSpaceRoom()))
|
initialState.eventSink(ConfigureRoomEvent.SetParentSpace(aSpaceRoom()))
|
||||||
skipItems(1) // Skip intermediate state
|
skipItems(1) // Skip intermediate state
|
||||||
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
|
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
|
||||||
|
|
||||||
// If we change the join rule back to public
|
// If we change the join rule back to public
|
||||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
|
||||||
skipItems(1) // Skip intermediate state (Public is still invalid)
|
skipItems(1) // Skip intermediate state (Public is still invalid)
|
||||||
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
|
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
|
||||||
|
|
||||||
// Then remove the parent space, the join rule stays private
|
// Then remove the parent space, the join rule stays private
|
||||||
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(null))
|
initialState.eventSink(ConfigureRoomEvent.SetParentSpace(null))
|
||||||
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
|
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -514,7 +514,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = initialState()
|
val initialState = initialState()
|
||||||
|
|
||||||
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(aSpaceRoom()))
|
initialState.eventSink(ConfigureRoomEvent.SetParentSpace(aSpaceRoom()))
|
||||||
|
|
||||||
assertThat(awaitError())
|
assertThat(awaitError())
|
||||||
}
|
}
|
||||||
@@ -534,8 +534,8 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
|
|||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = initialState()
|
val initialState = initialState()
|
||||||
|
|
||||||
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PrivateVisibility.Private))
|
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PrivateVisibility.Private))
|
||||||
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
|
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
|
||||||
|
|
||||||
assertThat(awaitItem().createRoomAction.isLoading()).isTrue()
|
assertThat(awaitItem().createRoomAction.isLoading()).isTrue()
|
||||||
assertThat(awaitItem().createRoomAction.isSuccess()).isTrue()
|
assertThat(awaitItem().createRoomAction.isSuccess()).isTrue()
|
||||||
|
|||||||
+5
-5
@@ -8,9 +8,9 @@
|
|||||||
|
|
||||||
package io.element.android.features.logout.impl
|
package io.element.android.features.logout.impl
|
||||||
|
|
||||||
sealed interface AccountDeactivationEvents {
|
sealed interface AccountDeactivationEvent {
|
||||||
data class SetEraseData(val eraseData: Boolean) : AccountDeactivationEvents
|
data class SetEraseData(val eraseData: Boolean) : AccountDeactivationEvent
|
||||||
data class SetPassword(val password: String) : AccountDeactivationEvents
|
data class SetPassword(val password: String) : AccountDeactivationEvent
|
||||||
data class DeactivateAccount(val isRetry: Boolean) : AccountDeactivationEvents
|
data class DeactivateAccount(val isRetry: Boolean) : AccountDeactivationEvent
|
||||||
data object CloseDialogs : AccountDeactivationEvents
|
data object CloseDialogs : AccountDeactivationEvent
|
||||||
}
|
}
|
||||||
+5
-5
@@ -34,19 +34,19 @@ class AccountDeactivationPresenter(
|
|||||||
|
|
||||||
val formState = remember { mutableStateOf(DeactivateFormState.Default) }
|
val formState = remember { mutableStateOf(DeactivateFormState.Default) }
|
||||||
|
|
||||||
fun handleEvent(event: AccountDeactivationEvents) {
|
fun handleEvent(event: AccountDeactivationEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is AccountDeactivationEvents.SetEraseData -> {
|
is AccountDeactivationEvent.SetEraseData -> {
|
||||||
updateFormState(formState) {
|
updateFormState(formState) {
|
||||||
copy(eraseData = event.eraseData)
|
copy(eraseData = event.eraseData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is AccountDeactivationEvents.SetPassword -> {
|
is AccountDeactivationEvent.SetPassword -> {
|
||||||
updateFormState(formState) {
|
updateFormState(formState) {
|
||||||
copy(password = event.password)
|
copy(password = event.password)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is AccountDeactivationEvents.DeactivateAccount ->
|
is AccountDeactivationEvent.DeactivateAccount ->
|
||||||
if (action.value.isConfirming() || event.isRetry) {
|
if (action.value.isConfirming() || event.isRetry) {
|
||||||
localCoroutineScope.deactivateAccount(
|
localCoroutineScope.deactivateAccount(
|
||||||
formState = formState.value,
|
formState = formState.value,
|
||||||
@@ -55,7 +55,7 @@ class AccountDeactivationPresenter(
|
|||||||
} else {
|
} else {
|
||||||
action.value = AsyncAction.ConfirmingNoParams
|
action.value = AsyncAction.ConfirmingNoParams
|
||||||
}
|
}
|
||||||
AccountDeactivationEvents.CloseDialogs -> {
|
AccountDeactivationEvent.CloseDialogs -> {
|
||||||
action.value = AsyncAction.Uninitialized
|
action.value = AsyncAction.Uninitialized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ import kotlinx.parcelize.Parcelize
|
|||||||
data class AccountDeactivationState(
|
data class AccountDeactivationState(
|
||||||
val deactivateFormState: DeactivateFormState,
|
val deactivateFormState: DeactivateFormState,
|
||||||
val accountDeactivationAction: AsyncAction<Unit>,
|
val accountDeactivationAction: AsyncAction<Unit>,
|
||||||
val eventSink: (AccountDeactivationEvents) -> Unit,
|
val eventSink: (AccountDeactivationEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
val submitEnabled: Boolean
|
val submitEnabled: Boolean
|
||||||
get() = accountDeactivationAction is AsyncAction.Uninitialized &&
|
get() = accountDeactivationAction is AsyncAction.Uninitialized &&
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@ package io.element.android.features.logout.impl
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
||||||
open class AccountDeactivationStateProvider : PreviewParameterProvider<AccountDeactivationState> {
|
open class AccountDeactivationStatePreviewParam : PreviewParameterProvider<AccountDeactivationState> {
|
||||||
private val filledForm = aDeactivateFormState(eraseData = true, password = "password")
|
private val filledForm = aDeactivateFormState(eraseData = true, password = "password")
|
||||||
override val values: Sequence<AccountDeactivationState>
|
override val values: Sequence<AccountDeactivationState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
@@ -45,7 +45,7 @@ internal fun aDeactivateFormState(
|
|||||||
internal fun anAccountDeactivationState(
|
internal fun anAccountDeactivationState(
|
||||||
deactivateFormState: DeactivateFormState = aDeactivateFormState(),
|
deactivateFormState: DeactivateFormState = aDeactivateFormState(),
|
||||||
accountDeactivationAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
accountDeactivationAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
eventSink: (AccountDeactivationEvents) -> Unit = {},
|
eventSink: (AccountDeactivationEvent) -> Unit = {},
|
||||||
) = AccountDeactivationState(
|
) = AccountDeactivationState(
|
||||||
deactivateFormState = deactivateFormState,
|
deactivateFormState = deactivateFormState,
|
||||||
accountDeactivationAction = accountDeactivationAction,
|
accountDeactivationAction = accountDeactivationAction,
|
||||||
+10
-10
@@ -102,14 +102,14 @@ fun AccountDeactivationView(
|
|||||||
Content(
|
Content(
|
||||||
state = state,
|
state = state,
|
||||||
onSubmitClick = {
|
onSubmitClick = {
|
||||||
eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
Buttons(
|
Buttons(
|
||||||
state = state,
|
state = state,
|
||||||
onSubmitClick = {
|
onSubmitClick = {
|
||||||
eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -117,13 +117,13 @@ fun AccountDeactivationView(
|
|||||||
AccountDeactivationActionDialog(
|
AccountDeactivationActionDialog(
|
||||||
state.accountDeactivationAction,
|
state.accountDeactivationAction,
|
||||||
onConfirmClick = {
|
onConfirmClick = {
|
||||||
eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
|
||||||
},
|
},
|
||||||
onRetryClick = {
|
onRetryClick = {
|
||||||
eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = true))
|
eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = true))
|
||||||
},
|
},
|
||||||
onDismissDialog = {
|
onDismissDialog = {
|
||||||
eventSink(AccountDeactivationEvents.CloseDialogs)
|
eventSink(AccountDeactivationEvent.CloseDialogs)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -239,7 +239,7 @@ private fun Content(
|
|||||||
headline = stringResource(R.string.screen_deactivate_account_delete_all_messages),
|
headline = stringResource(R.string.screen_deactivate_account_delete_all_messages),
|
||||||
value = eraseData,
|
value = eraseData,
|
||||||
onChange = {
|
onChange = {
|
||||||
eventSink(AccountDeactivationEvents.SetEraseData(it))
|
eventSink(AccountDeactivationEvent.SetEraseData(it))
|
||||||
},
|
},
|
||||||
enabled = !isLoading,
|
enabled = !isLoading,
|
||||||
)
|
)
|
||||||
@@ -275,7 +275,7 @@ private fun Content(
|
|||||||
onValueChange = {
|
onValueChange = {
|
||||||
val sanitized = it.sanitize()
|
val sanitized = it.sanitize()
|
||||||
passwordFieldState = sanitized
|
passwordFieldState = sanitized
|
||||||
eventSink(AccountDeactivationEvents.SetPassword(sanitized))
|
eventSink(AccountDeactivationEvent.SetPassword(sanitized))
|
||||||
},
|
},
|
||||||
placeholder = stringResource(CommonStrings.common_password),
|
placeholder = stringResource(CommonStrings.common_password),
|
||||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
@@ -299,16 +299,16 @@ private fun Content(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure that the string does not contain any new line characters, which can happen when pasting values.
|
* Ensure that the string does not contain any line separator, which can happen when pasting values.
|
||||||
*/
|
*/
|
||||||
private fun String.sanitize(): String {
|
private fun String.sanitize(): String {
|
||||||
return replace("\n", "")
|
return filterNot { it == '\n' || it == '\r' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AccountDeactivationViewPreview(
|
internal fun AccountDeactivationViewPreview(
|
||||||
@PreviewParameter(AccountDeactivationStateProvider::class) state: AccountDeactivationState,
|
@PreviewParameter(AccountDeactivationStatePreviewParam::class) state: AccountDeactivationState,
|
||||||
) = ElementPreview {
|
) = ElementPreview {
|
||||||
AccountDeactivationView(
|
AccountDeactivationView(
|
||||||
state,
|
state,
|
||||||
|
|||||||
+15
-15
@@ -47,11 +47,11 @@ class AccountDeactivationPresenterTest {
|
|||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.deactivateFormState).isEqualTo(DeactivateFormState.Default)
|
assertThat(initialState.deactivateFormState).isEqualTo(DeactivateFormState.Default)
|
||||||
initialState.eventSink(AccountDeactivationEvents.SetEraseData(true))
|
initialState.eventSink(AccountDeactivationEvent.SetEraseData(true))
|
||||||
val updatedState = awaitItem()
|
val updatedState = awaitItem()
|
||||||
assertThat(updatedState.deactivateFormState).isEqualTo(DeactivateFormState.Default.copy(eraseData = true))
|
assertThat(updatedState.deactivateFormState).isEqualTo(DeactivateFormState.Default.copy(eraseData = true))
|
||||||
assertThat(updatedState.submitEnabled).isFalse()
|
assertThat(updatedState.submitEnabled).isFalse()
|
||||||
updatedState.eventSink(AccountDeactivationEvents.SetPassword("password"))
|
updatedState.eventSink(AccountDeactivationEvent.SetPassword("password"))
|
||||||
val updatedState2 = awaitItem()
|
val updatedState2 = awaitItem()
|
||||||
assertThat(updatedState2.deactivateFormState).isEqualTo(DeactivateFormState(password = "password", eraseData = true))
|
assertThat(updatedState2.deactivateFormState).isEqualTo(DeactivateFormState(password = "password", eraseData = true))
|
||||||
assertThat(updatedState2.submitEnabled).isTrue()
|
assertThat(updatedState2.submitEnabled).isTrue()
|
||||||
@@ -71,12 +71,12 @@ class AccountDeactivationPresenterTest {
|
|||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(AccountDeactivationEvents.SetPassword("password"))
|
initialState.eventSink(AccountDeactivationEvent.SetPassword("password"))
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
initialState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
|
||||||
val updatedState = awaitItem()
|
val updatedState = awaitItem()
|
||||||
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
updatedState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
|
||||||
val updatedState2 = awaitItem()
|
val updatedState2 = awaitItem()
|
||||||
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
|
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
@@ -98,20 +98,20 @@ class AccountDeactivationPresenterTest {
|
|||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(AccountDeactivationEvents.SetPassword("password"))
|
initialState.eventSink(AccountDeactivationEvent.SetPassword("password"))
|
||||||
initialState.eventSink(AccountDeactivationEvents.SetEraseData(true))
|
initialState.eventSink(AccountDeactivationEvent.SetEraseData(true))
|
||||||
skipItems(2)
|
skipItems(2)
|
||||||
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
initialState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
|
||||||
val updatedState = awaitItem()
|
val updatedState = awaitItem()
|
||||||
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
updatedState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
|
||||||
val updatedState2 = awaitItem()
|
val updatedState2 = awaitItem()
|
||||||
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
|
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.accountDeactivationAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
|
assertThat(finalState.accountDeactivationAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
|
||||||
recorder.assertions().isCalledOnce().with(value("password"), value(true))
|
recorder.assertions().isCalledOnce().with(value("password"), value(true))
|
||||||
// Retry
|
// Retry
|
||||||
finalState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = true))
|
finalState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = true))
|
||||||
val finalState2 = awaitItem()
|
val finalState2 = awaitItem()
|
||||||
assertThat(finalState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
|
assertThat(finalState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
|
||||||
assertThat(awaitItem().accountDeactivationAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
|
assertThat(awaitItem().accountDeactivationAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
|
||||||
@@ -131,20 +131,20 @@ class AccountDeactivationPresenterTest {
|
|||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(AccountDeactivationEvents.SetPassword("password"))
|
initialState.eventSink(AccountDeactivationEvent.SetPassword("password"))
|
||||||
initialState.eventSink(AccountDeactivationEvents.SetEraseData(true))
|
initialState.eventSink(AccountDeactivationEvent.SetEraseData(true))
|
||||||
skipItems(2)
|
skipItems(2)
|
||||||
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
initialState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
|
||||||
val updatedState = awaitItem()
|
val updatedState = awaitItem()
|
||||||
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
|
||||||
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
|
updatedState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
|
||||||
val updatedState2 = awaitItem()
|
val updatedState2 = awaitItem()
|
||||||
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
|
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
|
||||||
val finalState = awaitItem()
|
val finalState = awaitItem()
|
||||||
assertThat(finalState.accountDeactivationAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
|
assertThat(finalState.accountDeactivationAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
|
||||||
recorder.assertions().isCalledOnce().with(value("password"), value(true))
|
recorder.assertions().isCalledOnce().with(value("password"), value(true))
|
||||||
// Cancel
|
// Cancel
|
||||||
finalState.eventSink(AccountDeactivationEvents.CloseDialogs)
|
finalState.eventSink(AccountDeactivationEvent.CloseDialogs)
|
||||||
val finalState2 = awaitItem()
|
val finalState2 = awaitItem()
|
||||||
assertThat(finalState2.accountDeactivationAction).isEqualTo(AsyncAction.Uninitialized)
|
assertThat(finalState2.accountDeactivationAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-13
@@ -35,7 +35,7 @@ import org.robolectric.annotation.Config
|
|||||||
class AccountDeactivationViewTest : RobolectricTest() {
|
class AccountDeactivationViewTest : RobolectricTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
|
fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
|
||||||
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>(expectEvents = false)
|
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>(expectEvents = false)
|
||||||
ensureCalledOnce {
|
ensureCalledOnce {
|
||||||
setAccountDeactivationView(
|
setAccountDeactivationView(
|
||||||
state = anAccountDeactivationState(eventSink = eventsRecorder),
|
state = anAccountDeactivationState(eventSink = eventsRecorder),
|
||||||
@@ -48,7 +48,7 @@ class AccountDeactivationViewTest : RobolectricTest() {
|
|||||||
@Config(qualifiers = "h1024dp")
|
@Config(qualifiers = "h1024dp")
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on Deactivate emits the expected Event`() = runAndroidComposeUiTest {
|
fun `clicking on Deactivate emits the expected Event`() = runAndroidComposeUiTest {
|
||||||
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
|
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>()
|
||||||
setAccountDeactivationView(
|
setAccountDeactivationView(
|
||||||
state = anAccountDeactivationState(
|
state = anAccountDeactivationState(
|
||||||
deactivateFormState = aDeactivateFormState(
|
deactivateFormState = aDeactivateFormState(
|
||||||
@@ -58,12 +58,12 @@ class AccountDeactivationViewTest : RobolectricTest() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
clickOn(CommonStrings.action_delete)
|
clickOn(CommonStrings.action_delete)
|
||||||
eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false))
|
eventsRecorder.assertSingle(AccountDeactivationEvent.DeactivateAccount(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on Deactivate on the confirmation dialog emits the expected Event`() = runAndroidComposeUiTest {
|
fun `clicking on Deactivate on the confirmation dialog emits the expected Event`() = runAndroidComposeUiTest {
|
||||||
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
|
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>()
|
||||||
setAccountDeactivationView(
|
setAccountDeactivationView(
|
||||||
state = anAccountDeactivationState(
|
state = anAccountDeactivationState(
|
||||||
deactivateFormState = aDeactivateFormState(
|
deactivateFormState = aDeactivateFormState(
|
||||||
@@ -74,12 +74,12 @@ class AccountDeactivationViewTest : RobolectricTest() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
pressTag(TestTags.dialogPositive.value)
|
pressTag(TestTags.dialogPositive.value)
|
||||||
eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false))
|
eventsRecorder.assertSingle(AccountDeactivationEvent.DeactivateAccount(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on retry on the confirmation dialog emits the expected Event`() = runAndroidComposeUiTest {
|
fun `clicking on retry on the confirmation dialog emits the expected Event`() = runAndroidComposeUiTest {
|
||||||
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
|
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>()
|
||||||
setAccountDeactivationView(
|
setAccountDeactivationView(
|
||||||
state = anAccountDeactivationState(
|
state = anAccountDeactivationState(
|
||||||
deactivateFormState = aDeactivateFormState(
|
deactivateFormState = aDeactivateFormState(
|
||||||
@@ -90,24 +90,24 @@ class AccountDeactivationViewTest : RobolectricTest() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
clickOn(CommonStrings.action_retry)
|
clickOn(CommonStrings.action_retry)
|
||||||
eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(true))
|
eventsRecorder.assertSingle(AccountDeactivationEvent.DeactivateAccount(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `switching on the erase all switch emits the expected Event`() = runAndroidComposeUiTest {
|
fun `switching on the erase all switch emits the expected Event`() = runAndroidComposeUiTest {
|
||||||
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
|
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>()
|
||||||
setAccountDeactivationView(
|
setAccountDeactivationView(
|
||||||
state = anAccountDeactivationState(
|
state = anAccountDeactivationState(
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
clickOn(R.string.screen_deactivate_account_delete_all_messages)
|
clickOn(R.string.screen_deactivate_account_delete_all_messages)
|
||||||
eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(true))
|
eventsRecorder.assertSingle(AccountDeactivationEvent.SetEraseData(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `switching off the erase all switch emits the expected Event`() = runAndroidComposeUiTest {
|
fun `switching off the erase all switch emits the expected Event`() = runAndroidComposeUiTest {
|
||||||
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
|
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>()
|
||||||
setAccountDeactivationView(
|
setAccountDeactivationView(
|
||||||
state = anAccountDeactivationState(
|
state = anAccountDeactivationState(
|
||||||
deactivateFormState = aDeactivateFormState(
|
deactivateFormState = aDeactivateFormState(
|
||||||
@@ -117,13 +117,13 @@ class AccountDeactivationViewTest : RobolectricTest() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
clickOn(R.string.screen_deactivate_account_delete_all_messages)
|
clickOn(R.string.screen_deactivate_account_delete_all_messages)
|
||||||
eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(false))
|
eventsRecorder.assertSingle(AccountDeactivationEvent.SetEraseData(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Config(qualifiers = "h1024dp")
|
@Config(qualifiers = "h1024dp")
|
||||||
@Test
|
@Test
|
||||||
fun `typing text in the password field emits the expected Event`() = runAndroidComposeUiTest {
|
fun `typing text in the password field emits the expected Event`() = runAndroidComposeUiTest {
|
||||||
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
|
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>()
|
||||||
setAccountDeactivationView(
|
setAccountDeactivationView(
|
||||||
state = anAccountDeactivationState(
|
state = anAccountDeactivationState(
|
||||||
deactivateFormState = aDeactivateFormState(
|
deactivateFormState = aDeactivateFormState(
|
||||||
@@ -133,7 +133,7 @@ class AccountDeactivationViewTest : RobolectricTest() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
onNodeWithTag(TestTags.loginPassword.value).performTextInput("A")
|
onNodeWithTag(TestTags.loginPassword.value).performTextInput("A")
|
||||||
eventsRecorder.assertSingle(AccountDeactivationEvents.SetPassword("A$A_PASSWORD"))
|
eventsRecorder.assertSingle(AccountDeactivationEvent.SetPassword("A$A_PASSWORD"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.enterprise.api
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClientBuilder
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook that can be used to customize the [MatrixClientBuilder] for enterprise features.
|
||||||
|
*/
|
||||||
|
fun interface ClientBuilderEnterpriseHook {
|
||||||
|
suspend operator fun invoke(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.enterprise.api
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook that can be used to customize the [MatrixClient] for enterprise features.
|
||||||
|
*/
|
||||||
|
fun interface ClientEnterpriseHook {
|
||||||
|
suspend operator fun invoke(client: MatrixClient)
|
||||||
|
}
|
||||||
+4
-13
@@ -10,7 +10,7 @@ package io.element.android.features.enterprise.api
|
|||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
import io.element.android.libraries.matrix.api.ClientUrlContentFetcher
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@@ -31,10 +31,9 @@ interface EnterpriseService {
|
|||||||
* Rewrites the authentication server URL when the deployment requires a different one from the one advertised.
|
* Rewrites the authentication server URL when the deployment requires a different one from the one advertised.
|
||||||
*
|
*
|
||||||
* @param url the URL to rewrite.
|
* @param url the URL to rewrite.
|
||||||
* @param homeserver the homeserver the URL belongs to.
|
|
||||||
* @param urlContentFetcher used to read the deployment configuration; a client that is not authenticated yet also works.
|
* @param urlContentFetcher used to read the deployment configuration; a client that is not authenticated yet also works.
|
||||||
*/
|
*/
|
||||||
suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher): String
|
suspend fun tweakMasUrl(url: String, urlContentFetcher: ClientUrlContentFetcher): String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of homeservers the user is allowed to sign in to.
|
* Returns the list of homeservers the user is allowed to sign in to.
|
||||||
@@ -53,9 +52,9 @@ interface EnterpriseService {
|
|||||||
/**
|
/**
|
||||||
* Whether the given homeserver enforces the use of Element Pro or a derived app.
|
* Whether the given homeserver enforces the use of Element Pro or a derived app.
|
||||||
*
|
*
|
||||||
* @param homeserverUrl the homeserver to check.
|
* @param serverName the homeserver to check.
|
||||||
*/
|
*/
|
||||||
suspend fun isElementProEnforced(homeserverUrl: String): Boolean
|
suspend fun isElementProEnforced(serverName: String): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override the brand color.
|
* Override the brand color.
|
||||||
@@ -99,14 +98,6 @@ interface EnterpriseService {
|
|||||||
*/
|
*/
|
||||||
fun getNoisyNotificationChannelId(sessionId: SessionId): String?
|
fun getNoisyNotificationChannelId(sessionId: SessionId): String?
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the Element Server Suite (ESS) config endpoint URL for the given domain.
|
|
||||||
* Returns `null` when this build does not read its configuration from an ESS deployment.
|
|
||||||
*
|
|
||||||
* @param domain the server whose configuration endpoint is requested.
|
|
||||||
*/
|
|
||||||
fun essConfigEndpointUrl(domain: String): String?
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ANY_ACCOUNT_PROVIDER = "*"
|
const val ANY_ACCOUNT_PROVIDER = "*"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -32,13 +32,13 @@ sealed interface RemoteEnterpriseConfigResult<out T> {
|
|||||||
is Success<T> -> data
|
is Success<T> -> data
|
||||||
is Outdated<T> -> data
|
is Outdated<T> -> data
|
||||||
is Error -> null
|
is Error -> null
|
||||||
NotFound -> null
|
is NotFound -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun upToDateDataOrNull(): T? = when (this) {
|
fun upToDateDataOrNull(): T? = when (this) {
|
||||||
is Success<T> -> data
|
is Success<T> -> data
|
||||||
is Outdated<T> -> null
|
is Outdated<T> -> null
|
||||||
is Error -> null
|
is Error -> null
|
||||||
NotFound -> null
|
is NotFound -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.enterprise.impl
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import io.element.android.features.enterprise.api.ClientBuilderEnterpriseHook
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClientBuilder
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of [ClientBuilderEnterpriseHook] that does nothing for FOSS.
|
||||||
|
*/
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultClientBuilderEnterpriseHook : ClientBuilderEnterpriseHook {
|
||||||
|
override suspend fun invoke(clientBuilder: MatrixClientBuilder, sessionId: SessionId): MatrixClientBuilder {
|
||||||
|
// No modification
|
||||||
|
return clientBuilder
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations 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.enterprise.impl
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import io.element.android.features.enterprise.api.ClientEnterpriseHook
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of [ClientEnterpriseHook] that does nothing for FOSS.
|
||||||
|
*/
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultClientEnterpriseHook : ClientEnterpriseHook {
|
||||||
|
override suspend fun invoke(client: MatrixClient) {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-5
@@ -14,7 +14,7 @@ import dev.zacsweers.metro.ContributesBinding
|
|||||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||||
import io.element.android.features.enterprise.api.BugReportUrl
|
import io.element.android.features.enterprise.api.BugReportUrl
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
import io.element.android.libraries.matrix.api.ClientUrlContentFetcher
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
@@ -22,10 +22,10 @@ import kotlinx.coroutines.flow.flowOf
|
|||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultEnterpriseService : EnterpriseService {
|
class DefaultEnterpriseService : EnterpriseService {
|
||||||
override suspend fun isEnterpriseUser(sessionId: SessionId) = false
|
override suspend fun isEnterpriseUser(sessionId: SessionId) = false
|
||||||
override suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher) = url
|
override suspend fun tweakMasUrl(url: String, urlContentFetcher: ClientUrlContentFetcher) = url
|
||||||
override fun homeserverAllowList(): List<String> = emptyList()
|
override fun homeserverAllowList(): List<String> = emptyList()
|
||||||
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
|
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
|
||||||
override suspend fun isElementProEnforced(homeserverUrl: String): Boolean = false
|
override suspend fun isElementProEnforced(serverName: String): Boolean = false
|
||||||
|
|
||||||
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = Unit
|
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = Unit
|
||||||
|
|
||||||
@@ -45,6 +45,4 @@ class DefaultEnterpriseService : EnterpriseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null
|
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null
|
||||||
|
|
||||||
override fun essConfigEndpointUrl(domain: String): String? = null
|
|
||||||
}
|
}
|
||||||
|
|||||||
-29
@@ -1,29 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2026 Element Creations 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.enterprise.impl.scanner
|
|
||||||
|
|
||||||
import dev.zacsweers.metro.AppScope
|
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
|
||||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
|
||||||
import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default FOSS implementation of [ContentScannerUrlProvider] that returns `null` for the content scanner URL.
|
|
||||||
*/
|
|
||||||
@ContributesBinding(AppScope::class)
|
|
||||||
class DefaultContentScannerUrlProvider : ContentScannerUrlProvider {
|
|
||||||
override suspend fun getContentScannerUrl(sessionId: SessionId): Result<String?> = Result.success(null)
|
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
|
||||||
class Factory : ContentScannerUrlProvider.Factory {
|
|
||||||
override fun create(urlContentFetcher: UrlContentFetcher): ContentScannerUrlProvider {
|
|
||||||
return DefaultContentScannerUrlProvider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+6
-11
@@ -12,7 +12,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||||
import io.element.android.features.enterprise.api.BugReportUrl
|
import io.element.android.features.enterprise.api.BugReportUrl
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
import io.element.android.libraries.matrix.api.ClientUrlContentFetcher
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
@@ -30,8 +30,7 @@ class FakeEnterpriseService(
|
|||||||
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
|
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
|
||||||
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
|
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
|
||||||
private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() },
|
private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() },
|
||||||
private val tweakMasUrlResult: (String, String, UrlContentFetcher) -> String = { _, _, _ -> lambdaError() },
|
private val tweakMasUrlResult: (String, ClientUrlContentFetcher) -> String = { _, _ -> lambdaError() },
|
||||||
private val essConfigEndpointUrlResult: (String) -> String = { lambdaError() },
|
|
||||||
private val isElementProEnforcedResult: (String) -> Boolean = { lambdaError() },
|
private val isElementProEnforcedResult: (String) -> Boolean = { lambdaError() },
|
||||||
) : EnterpriseService {
|
) : EnterpriseService {
|
||||||
private val brandColorState = MutableStateFlow(initialBrandColor)
|
private val brandColorState = MutableStateFlow(initialBrandColor)
|
||||||
@@ -41,8 +40,8 @@ class FakeEnterpriseService(
|
|||||||
isEnterpriseUserResult(sessionId)
|
isEnterpriseUserResult(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher): String = simulateLongTask {
|
override suspend fun tweakMasUrl(url: String, urlContentFetcher: ClientUrlContentFetcher): String = simulateLongTask {
|
||||||
tweakMasUrlResult(url, homeserver, urlContentFetcher)
|
tweakMasUrlResult(url, urlContentFetcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun homeserverAllowList(): List<String> {
|
override fun homeserverAllowList(): List<String> {
|
||||||
@@ -53,8 +52,8 @@ class FakeEnterpriseService(
|
|||||||
isAllowedToConnectToHomeserverResult(homeserverUrl)
|
isAllowedToConnectToHomeserverResult(homeserverUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun isElementProEnforced(homeserverUrl: String): Boolean = simulateLongTask {
|
override suspend fun isElementProEnforced(serverName: String): Boolean = simulateLongTask {
|
||||||
isElementProEnforcedResult(homeserverUrl)
|
isElementProEnforcedResult(serverName)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = simulateLongTask {
|
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = simulateLongTask {
|
||||||
@@ -85,8 +84,4 @@ class FakeEnterpriseService(
|
|||||||
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? {
|
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? {
|
||||||
return getNoisyNotificationChannelIdResult(sessionId)
|
return getNoisyNotificationChannelIdResult(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun essConfigEndpointUrl(domain: String): String {
|
|
||||||
return essConfigEndpointUrlResult(domain)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -8,6 +8,6 @@
|
|||||||
|
|
||||||
package io.element.android.features.forward.impl
|
package io.element.android.features.forward.impl
|
||||||
|
|
||||||
sealed interface ForwardMessagesEvents {
|
sealed interface ForwardMessagesEvent {
|
||||||
data object ClearError : ForwardMessagesEvents
|
data object ClearError : ForwardMessagesEvent
|
||||||
}
|
}
|
||||||
+2
-2
@@ -48,9 +48,9 @@ class ForwardMessagesPresenter(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ForwardMessagesState {
|
override fun present(): ForwardMessagesState {
|
||||||
fun handleEvent(event: ForwardMessagesEvents) {
|
fun handleEvent(event: ForwardMessagesEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
ForwardMessagesEvents.ClearError -> forwardingActionState.value = AsyncAction.Uninitialized
|
ForwardMessagesEvent.ClearError -> forwardingActionState.value = AsyncAction.Uninitialized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -13,5 +13,5 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||||||
|
|
||||||
data class ForwardMessagesState(
|
data class ForwardMessagesState(
|
||||||
val forwardAction: AsyncAction<List<RoomId>>,
|
val forwardAction: AsyncAction<List<RoomId>>,
|
||||||
val eventSink: (ForwardMessagesEvents) -> Unit
|
val eventSink: (ForwardMessagesEvent) -> Unit
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
open class ForwardMessagesStateProvider : PreviewParameterProvider<ForwardMessagesState> {
|
open class ForwardMessagesStatePreviewParam : PreviewParameterProvider<ForwardMessagesState> {
|
||||||
override val values: Sequence<ForwardMessagesState>
|
override val values: Sequence<ForwardMessagesState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aForwardMessagesState(),
|
aForwardMessagesState(),
|
||||||
@@ -32,7 +32,7 @@ open class ForwardMessagesStateProvider : PreviewParameterProvider<ForwardMessag
|
|||||||
|
|
||||||
fun aForwardMessagesState(
|
fun aForwardMessagesState(
|
||||||
forwardAction: AsyncAction<List<RoomId>> = AsyncAction.Uninitialized,
|
forwardAction: AsyncAction<List<RoomId>> = AsyncAction.Uninitialized,
|
||||||
eventSink: (ForwardMessagesEvents) -> Unit = {}
|
eventSink: (ForwardMessagesEvent) -> Unit = {}
|
||||||
) = ForwardMessagesState(
|
) = ForwardMessagesState(
|
||||||
forwardAction = forwardAction,
|
forwardAction = forwardAction,
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
+2
-2
@@ -31,14 +31,14 @@ fun ForwardMessagesView(
|
|||||||
stringResource(id = CommonStrings.error_unknown)
|
stringResource(id = CommonStrings.error_unknown)
|
||||||
},
|
},
|
||||||
onErrorDismiss = {
|
onErrorDismiss = {
|
||||||
state.eventSink(ForwardMessagesEvents.ClearError)
|
state.eventSink(ForwardMessagesEvent.ClearError)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ForwardMessagesViewPreview(@PreviewParameter(ForwardMessagesStateProvider::class) state: ForwardMessagesState) = ElementPreview {
|
internal fun ForwardMessagesViewPreview(@PreviewParameter(ForwardMessagesStatePreviewParam::class) state: ForwardMessagesState) = ElementPreview {
|
||||||
ForwardMessagesView(
|
ForwardMessagesView(
|
||||||
state = state,
|
state = state,
|
||||||
onForwardSuccess = {}
|
onForwardSuccess = {}
|
||||||
|
|||||||
+1
-1
@@ -86,7 +86,7 @@ class ForwardMessagesPresenterTest {
|
|||||||
val failedForwardState = awaitItem()
|
val failedForwardState = awaitItem()
|
||||||
assertThat(failedForwardState.forwardAction.isFailure()).isTrue()
|
assertThat(failedForwardState.forwardAction.isFailure()).isTrue()
|
||||||
// Then clear error
|
// Then clear error
|
||||||
failedForwardState.eventSink(ForwardMessagesEvents.ClearError)
|
failedForwardState.eventSink(ForwardMessagesEvent.ClearError)
|
||||||
assertThat(awaitItem().forwardAction.isUninitialized()).isTrue()
|
assertThat(awaitItem().forwardAction.isUninitialized()).isTrue()
|
||||||
forwardEventLambda.assertions().isCalledOnce()
|
forwardEventLambda.assertions().isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -29,7 +29,7 @@ import org.junit.Test
|
|||||||
class ForwardMessagesViewTest : RobolectricTest() {
|
class ForwardMessagesViewTest : RobolectricTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `cancel error emits the expected event`() = runAndroidComposeUiTest {
|
fun `cancel error emits the expected event`() = runAndroidComposeUiTest {
|
||||||
val eventsRecorder = EventsRecorder<ForwardMessagesEvents>()
|
val eventsRecorder = EventsRecorder<ForwardMessagesEvent>()
|
||||||
setForwardMessagesView(
|
setForwardMessagesView(
|
||||||
aForwardMessagesState(
|
aForwardMessagesState(
|
||||||
forwardAction = AsyncAction.Failure(AN_EXCEPTION),
|
forwardAction = AsyncAction.Failure(AN_EXCEPTION),
|
||||||
@@ -37,13 +37,13 @@ class ForwardMessagesViewTest : RobolectricTest() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
pressTag(TestTags.dialogPositive.value)
|
pressTag(TestTags.dialogPositive.value)
|
||||||
eventsRecorder.assertSingle(ForwardMessagesEvents.ClearError)
|
eventsRecorder.assertSingle(ForwardMessagesEvent.ClearError)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `success invokes onForwardSuccess`() = runAndroidComposeUiTest {
|
fun `success invokes onForwardSuccess`() = runAndroidComposeUiTest {
|
||||||
val data = listOf(A_ROOM_ID)
|
val data = listOf(A_ROOM_ID)
|
||||||
val eventsRecorder = EventsRecorder<ForwardMessagesEvents>(expectEvents = false)
|
val eventsRecorder = EventsRecorder<ForwardMessagesEvent>(expectEvents = false)
|
||||||
ensureCalledOnceWithParam<List<RoomId>?>(data) { callback ->
|
ensureCalledOnceWithParam<List<RoomId>?>(data) { callback ->
|
||||||
setForwardMessagesView(
|
setForwardMessagesView(
|
||||||
aForwardMessagesState(
|
aForwardMessagesState(
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user