diff --git a/.github/renovate.json5 b/.github/renovate.json5
index c16310cf43..86ab2f1b1e 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -7,7 +7,6 @@
"PR-Dependencies",
],
"ignoreDeps": [
- "string:app_name",
"gradle",
],
"minimumReleaseAge": "7 days",
diff --git a/.gitignore b/.gitignore
index 77acaadb20..a50ae12c6e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,9 +31,6 @@ __pycache__/
# Local configuration file (sdk path, etc)
local.properties
-# Proguard folder generated by Eclipse
-proguard/
-
# Log Files
*.log
diff --git a/.idea/copyright/Element_Enterprise.xml b/.idea/copyright/Element_Enterprise.xml
index b556049368..f7637c76a5 100644
--- a/.idea/copyright/Element_Enterprise.xml
+++ b/.idea/copyright/Element_Enterprise.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/.maestro/tests/account/changeServer.yaml b/.maestro/tests/account/changeServer.yaml
deleted file mode 100644
index b07fa5cc09..0000000000
--- a/.maestro/tests/account/changeServer.yaml
+++ /dev/null
@@ -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"
diff --git a/.maestro/tests/account/login.yaml b/.maestro/tests/account/login.yaml
index 873843b22d..81922c7f38 100644
--- a/.maestro/tests/account/login.yaml
+++ b/.maestro/tests/account/login.yaml
@@ -3,8 +3,7 @@ appId: ${MAESTRO_APP_ID}
- tapOn: "Sign in manually"
- runFlow: ../assertions/assertLoginDisplayed.yaml
- takeScreenshot: build/maestro/100-SignIn
-- runFlow: changeServer.yaml
-- runFlow: ../assertions/assertLoginDisplayed.yaml
+# The account provider field defaults to matrix.org, so continue straight to the MAS login.
- tapOn:
id: "login-continue"
## MAS page
diff --git a/.maestro/tests/assertions/assertLoginDisplayed.yaml b/.maestro/tests/assertions/assertLoginDisplayed.yaml
index 6d8558c38e..c32f23c323 100644
--- a/.maestro/tests/assertions/assertLoginDisplayed.yaml
+++ b/.maestro/tests/assertions/assertLoginDisplayed.yaml
@@ -1,5 +1,5 @@
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
- visible: "Change account provider"
+ visible: "Enter your account provider"
timeout: 10000
diff --git a/AGENTS.md b/AGENTS.md
index d206be98c6..917285ab30 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -37,8 +37,9 @@ PRs must meet these rules.
- Create previews for **all main states** of a Composable.
- Use `@PreviewsDayNight` for consistency.
-- Use `PreviewParameterProvider` (e.g., `FooStateProvider`) to provide states.
+- Use `PreviewParameterProvider` (e.g., `FooStatePreviewParam`) to provide states.
- 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`. |
| `FooState.kt` | Data class representing the immutable UI state. |
| `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. |
---
diff --git a/CHANGES.md b/CHANGES.md
index 2955fc8ad7..db18e59251 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,86 @@
+Changes in Element X 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
=============================
diff --git a/annotations/src/main/kotlin/io/element/android/annotations/ContributesNode.kt b/annotations/src/main/kotlin/io/element/android/annotations/ContributesNode.kt
index 632bdc3e81..8091206cbd 100644
--- a/annotations/src/main/kotlin/io/element/android/annotations/ContributesNode.kt
+++ b/annotations/src/main/kotlin/io/element/android/annotations/ContributesNode.kt
@@ -14,16 +14,16 @@ import kotlin.reflect.KClass
* Adds Node to the specified component graph.
* Equivalent to the following declaration:
*
+ * ```
* @BindingContainer
* @ContributesTo(Scope::class)
- * abstract class YourNodeModule {
-
- * @Binds
- * @IntoMap
- * @NodeKey(YourNode::class)
- * abstract fun bindYourNodeFactory(factory: YourNode.Factory): AssistedNodeFactory<*>
- *}
-
+ * interface YourNodeBindingContainer {
+ * @Binds
+ * @IntoMap
+ * @NodeKey(YourNode::class)
+ * fun bindYourNodeAssistedFactory(factory: YourNodeAssistedFactory): AssistedNodeFactory<*>
+ * }
+ * ```
*/
@Target(AnnotationTarget.CLASS)
annotation class ContributesNode(
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index f2c7d37baa..f85d8dc33c 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -127,24 +127,11 @@ android {
optimization {
enable = true
keepRules {
- files.add(File(projectDir, "common-proguard-rules.pro"))
- files.add(getDefaultProguardFile("proguard-android-optimize.txt"))
-
- // 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")
- }
+ // Equivalent of adding `getDefaultProguardFile("proguard-android-optimize.txt")` (this is the default value).
+ includeDefault = true
}
+ // 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 ->
+ // 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
// other than the universal APK.
variant.outputs.forEach { output ->
@@ -259,7 +270,6 @@ dependencies {
if (isEnterpriseBuild) {
allEnterpriseImpl(project)
implementation(projects.appicon.enterprise)
- implementation(project(":enterprise:features:enterprise:shared"))
} else {
implementation(projects.features.enterprise.implFoss)
implementation(projects.appicon.element)
diff --git a/app/common-proguard-rules.pro b/app/proguard/common/common-proguard-rules.keep
similarity index 100%
rename from app/common-proguard-rules.pro
rename to app/proguard/common/common-proguard-rules.keep
diff --git a/app/default-proguard-rules.pro b/app/proguard/foss/default-proguard-rules.keep
similarity index 100%
rename from app/default-proguard-rules.pro
rename to app/proguard/foss/default-proguard-rules.keep
diff --git a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt b/app/src/main/kotlin/io/element/android/x/di/AppBindingContainer.kt
similarity index 97%
rename from app/src/main/kotlin/io/element/android/x/di/AppModule.kt
rename to app/src/main/kotlin/io/element/android/x/di/AppBindingContainer.kt
index 39c70e35d8..3f8e94d8ef 100644
--- a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt
+++ b/app/src/main/kotlin/io/element/android/x/di/AppBindingContainer.kt
@@ -38,7 +38,7 @@ import java.io.File
@BindingContainer
@ContributesTo(AppScope::class)
-object AppModule {
+object AppBindingContainer {
@Provides
@BaseDirectory
fun providesBaseDirectory(@ApplicationContext context: Context): File {
@@ -111,7 +111,7 @@ object AppModule {
@Provides
@SingleIn(AppScope::class)
- fun provideSnackbarDispatcher(): SnackbarDispatcher {
+ fun providesSnackbarDispatcher(): SnackbarDispatcher {
return SnackbarDispatcher()
}
}
diff --git a/appconfig/src/main/kotlin/io/element/android/appconfig/PushConfig.kt b/appconfig/src/main/kotlin/io/element/android/appconfig/PushConfig.kt
deleted file mode 100644
index d36fb742b0..0000000000
--- a/appconfig/src/main/kotlin/io/element/android/appconfig/PushConfig.kt
+++ /dev/null
@@ -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"
-}
diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts
index 054e9525c7..12b95d9a09 100644
--- a/appnav/build.gradle.kts
+++ b/appnav/build.gradle.kts
@@ -61,17 +61,23 @@ dependencies {
testCommonDependencies(libs)
testImplementation(projects.features.login.test)
testImplementation(projects.features.share.test)
+ testImplementation(projects.libraries.accountselect.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test)
+ testImplementation(projects.libraries.matrixmedia.test)
testImplementation(projects.libraries.oauth.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test)
+ testImplementation(projects.libraries.sessionStorage.test)
+ testImplementation(projects.features.announcement.test)
testImplementation(projects.features.forward.test)
testImplementation(projects.features.messages.test)
testImplementation(projects.features.networkmonitor.test)
+ testImplementation(projects.features.preferences.test)
testImplementation(projects.features.rageshake.test)
+ testImplementation(projects.features.signedout.test)
testImplementation(projects.services.apperror.test)
testImplementation(projects.services.appnavstate.test)
testImplementation(projects.services.analytics.test)
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
index cd071131d9..dab9b7828c 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
@@ -335,11 +335,11 @@ class LoggedInFlowNode(
}
NavTarget.Home -> {
val callback = object : HomeEntryPoint.Callback {
- override fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) {
+ override fun navigateToRoom(roomId: RoomId, eventId: EventId?, joinedRoom: JoinedRoom?) {
lifecycleScope.launch {
attachRoom(
roomIdOrAlias = roomId.toRoomIdOrAlias(),
- initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom),
+ initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom, eventId = eventId),
clearBackstack = false,
)
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
index acf7b66db9..c9deaeb3b3 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt
@@ -32,7 +32,6 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
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.ResolvedIntent
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.RootPresenter
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.login.api.LoginParams
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
@@ -109,6 +109,13 @@ class RootFlowNode(
buildContext = buildContext,
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() {
analyticsColdStartWatcher.start()
appCoroutineScope.launch {
@@ -152,7 +159,7 @@ class RootFlowNode(
}
}
LoggedInState.NotLoggedIn -> {
- switchToNotLoggedInFlow(null)
+ switchToNotLoggedInFlow(pendingLoginParams)
}
}
}
@@ -194,15 +201,18 @@ class RootFlowNode(
}
private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) {
+ pendingLoginParams = null
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId))
}
private fun switchToNotLoggedInFlow(params: LoginParams?) {
+ Timber.d("switchToNotLoggedInFlow, hasLoginParams=${params != null}")
matrixSessionCache.removeAll()
backstack.safeRoot(NavTarget.NotLoggedInFlow(params))
}
private fun switchToSignedOutFlow(sessionId: SessionId) {
+ pendingLoginParams = null
backstack.safeRoot(NavTarget.SignedOutFlow(sessionId))
}
@@ -319,6 +329,7 @@ class RootFlowNode(
}
override fun onDone() {
+ pendingLoginParams = null
backstack.pop()
}
}
@@ -418,6 +429,7 @@ class RootFlowNode(
Timber.w("Login link ignored, multi account is disabled")
}
} else {
+ pendingLoginParams = params
switchToNotLoggedInFlow(params)
}
} else {
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
index ee316f00aa..5d3a85fa9a 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/intent/IntentResolver.kt
@@ -76,6 +76,7 @@ class IntentResolver(
}
private fun Intent.canBeIgnored(): Boolean {
+ if (flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) return true
return action == Intent.ACTION_MAIN &&
categories?.contains(Intent.CATEGORY_LAUNCHER) == true
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvent.kt
similarity index 90%
rename from appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt
rename to appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvent.kt
index 66fffce9e8..34ba2e6460 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvent.kt
@@ -8,11 +8,11 @@
package io.element.android.appnav.loggedin
-sealed interface LoggedInEvents {
- data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvents
- data object CheckSlidingSyncProxyAvailability : LoggedInEvents
- data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvents
- data object DismissLocalNetworkPermissionPrompt : LoggedInEvents
+sealed interface LoggedInEvent {
+ data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvent
+ data object CheckSlidingSyncProxyAvailability : LoggedInEvent
+ data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvent
+ data object DismissLocalNetworkPermissionPrompt : LoggedInEvent
- data object RequestLocationNetworkPermission : LoggedInEvents
+ data object RequestLocationNetworkPermission : LoggedInEvent
}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt
index d46fb2f931..f6faee0acf 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt
@@ -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.log.logger.LoggerTag
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.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
@@ -67,7 +65,6 @@ class LoggedInPresenter(
private val buildMeta: BuildMeta,
private val networkMonitor: NetworkMonitor,
private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor,
- private val featureFlagService: FeatureFlagService,
permissionsPresenterFactory: PermissionsPresenter.Factory,
) : Presenter {
private val localNetworkPermissionsPresenter: PermissionsPresenter =
@@ -124,13 +121,9 @@ class LoggedInPresenter(
}
LaunchedEffect(Unit) {
- // Keep automatic call status (m.call) in sync with the feature flag and homeserver support.
- featureFlagService.isFeatureEnabledFlow(FeatureFlags.UserStatus)
- .onEach { isUserStatusEnabled ->
- val enabled = isUserStatusEnabled && matrixClient.isUserStatusSupported().getOrDefault(false)
- matrixClient.enableAutomaticCallStatus(enabled)
- }
- .launchIn(this)
+ // Keep automatic call status (m.call) in sync with homeserver support.
+ val enabled = matrixClient.isUserStatusSupported().getOrDefault(false)
+ matrixClient.enableAutomaticCallStatus(enabled)
}
val networkConnectivity by networkMonitor.connectivity.collectAsState()
@@ -156,9 +149,9 @@ class LoggedInPresenter(
else -> LocalNetworkPermissionDialog.Settings
}
- fun handleEvent(event: LoggedInEvents) {
+ fun handleEvent(event: LoggedInEvent) {
when (event) {
- is LoggedInEvents.CloseErrorDialog -> {
+ is LoggedInEvent.CloseErrorDialog -> {
pusherRegistrationState.value = AsyncData.Uninitialized
if (event.doNotShowAgain) {
coroutineScope.launch {
@@ -166,17 +159,17 @@ class LoggedInPresenter(
}
}
}
- LoggedInEvents.CheckSlidingSyncProxyAvailability -> coroutineScope.launch {
+ LoggedInEvent.CheckSlidingSyncProxyAvailability -> coroutineScope.launch {
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
matrixClient.logout(userInitiated = true, ignoreSdkError = true)
}
- LoggedInEvents.DismissLocalNetworkPermissionPrompt -> {
+ LoggedInEvent.DismissLocalNetworkPermissionPrompt -> {
localNetworkPromptDismissedThisSession = true
}
- LoggedInEvents.RequestLocationNetworkPermission -> {
+ LoggedInEvent.RequestLocationNetworkPermission -> {
if (localNetworkPermissionDialog == LocalNetworkPermissionDialog.Settings) {
localNetworkPermissionState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog)
} else {
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt
index edc12383bf..33499341e7 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt
@@ -18,5 +18,5 @@ data class LoggedInState(
val forceNativeSlidingSyncMigration: Boolean,
val appName: String,
val localNetworkPermissionDialog: LocalNetworkPermissionDialog,
- val eventSink: (LoggedInEvents) -> Unit,
+ val eventSink: (LoggedInEvent) -> Unit,
)
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStatePreviewParam.kt
similarity index 95%
rename from appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt
rename to appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStatePreviewParam.kt
index 67fcb95c2c..b479e00279 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStatePreviewParam.kt
@@ -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.push.api.PusherRegistrationFailure
-open class LoggedInStateProvider : PreviewParameterProvider {
+open class LoggedInStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aLoggedInState(),
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt
index e57d378da4..060a24855a 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt
@@ -37,7 +37,7 @@ fun LoggedInView(
) {
OnLifecycleEvent { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
- state.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
+ state.eventSink(LoggedInEvent.CheckSlidingSyncProxyAvailability)
}
}
Box(
@@ -63,10 +63,10 @@ fun LoggedInView(
content = stringResource(id = CommonStrings.common_error_registering_pusher_android, reason),
cancelText = stringResource(id = CommonStrings.common_settings),
onDismiss = {
- state.eventSink(LoggedInEvents.CloseErrorDialog(it))
+ state.eventSink(LoggedInEvent.CloseErrorDialog(it))
},
onCancel = {
- state.eventSink(LoggedInEvents.CloseErrorDialog(false))
+ state.eventSink(LoggedInEvent.CloseErrorDialog(false))
navigateToNotificationTroubleshoot()
}
)
@@ -79,15 +79,15 @@ fun LoggedInView(
ForceNativeSlidingSyncMigrationDialog(
appName = state.appName,
onSubmit = {
- state.eventSink(LoggedInEvents.LogoutAndMigrateToNativeSlidingSync)
+ state.eventSink(LoggedInEvent.LogoutAndMigrateToNativeSlidingSync)
}
)
}
LocalNetworkPermissionDialogView(
dialog = state.localNetworkPermissionDialog,
- onSubmit = { state.eventSink(LoggedInEvents.RequestLocationNetworkPermission) },
- onDismiss = { state.eventSink(LoggedInEvents.DismissLocalNetworkPermissionPrompt) },
+ onSubmit = { state.eventSink(LoggedInEvent.RequestLocationNetworkPermission) },
+ onDismiss = { state.eventSink(LoggedInEvent.DismissLocalNetworkPermissionPrompt) },
)
}
@@ -124,7 +124,7 @@ private fun ForceNativeSlidingSyncMigrationDialog(
@PreviewsDayNight
@Composable
-internal fun LoggedInViewPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) = ElementPreview {
+internal fun LoggedInViewPreview(@PreviewParameter(LoggedInStatePreviewParam::class) state: LoggedInState) = ElementPreview {
LoggedInView(
state = state,
navigateToNotificationTroubleshoot = {},
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt
index a596052fca..175b662e08 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/LoadingRoomNodeView.kt
@@ -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.utils.DelayedVisibility
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
@Composable
@@ -84,7 +84,7 @@ private fun LoadingRoomTopBar(
@PreviewsDayNight
@Composable
-internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) = ElementPreview {
+internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStatePreviewParam::class) state: LoadingRoomState) = ElementPreview {
LoadingRoomNodeView(
state = state,
onBackClick = {}
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt
index 9a91e9765b..8b02752a64 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt
@@ -11,7 +11,7 @@ package io.element.android.appnav.root
import com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.core.state.SavedStateMap
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.libraries.matrix.ui.media.ImageLoaderHolder
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootStatePreviewParam.kt
similarity index 95%
rename from appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt
rename to appnav/src/main/kotlin/io/element/android/appnav/root/RootStatePreviewParam.kt
index 26db205498..8ae3d261e8 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootStateProvider.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootStatePreviewParam.kt
@@ -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.aAppErrorState
-open class RootStateProvider : PreviewParameterProvider {
+open class RootStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aRootState().copy(
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt
index 19aa820433..aa6bcf1a88 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootView.kt
@@ -60,7 +60,7 @@ fun RootView(
@PreviewsDayNight
@Composable
-internal fun RootViewPreview(@PreviewParameter(RootStateProvider::class) rootState: RootState) = ElementPreview {
+internal fun RootViewPreview(@PreviewParameter(RootStatePreviewParam::class) rootState: RootState) = ElementPreview {
RootView(
state = rootState,
onOpenBugReport = {},
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt b/appnav/src/main/kotlin/io/element/android/appnav/session/MatrixSessionCache.kt
similarity index 98%
rename from appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt
rename to appnav/src/main/kotlin/io/element/android/appnav/session/MatrixSessionCache.kt
index 11337f2e0a..ffae5f2b17 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixSessionCache.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/session/MatrixSessionCache.kt
@@ -6,7 +6,7 @@
* 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 com.bumble.appyx.core.state.MutableSavedStateMap
@@ -115,7 +115,7 @@ class MatrixSessionCache(
private fun onNewMatrixClient(matrixClient: MatrixClient) {
val syncOrchestrator = syncOrchestratorFactory.create(
- syncService = matrixClient.syncService,
+ matrixClient = matrixClient,
sessionCoroutineScope = matrixClient.sessionCoroutineScope,
)
sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt b/appnav/src/main/kotlin/io/element/android/appnav/session/SyncOrchestrator.kt
similarity index 91%
rename from appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt
rename to appnav/src/main/kotlin/io/element/android/appnav/session/SyncOrchestrator.kt
index 1ce423a569..3a72c10ab7 100644
--- a/appnav/src/main/kotlin/io/element/android/appnav/di/SyncOrchestrator.kt
+++ b/appnav/src/main/kotlin/io/element/android/appnav/session/SyncOrchestrator.kt
@@ -6,7 +6,7 @@
* 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 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.libraries.core.coroutine.CoroutineDispatchers
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.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.recordTransaction
@@ -37,7 +37,7 @@ import kotlin.time.Duration.Companion.seconds
@AssistedInject
class SyncOrchestrator(
- @Assisted private val syncService: SyncService,
+ @Assisted private val matrixClient: MatrixClient,
@Assisted sessionCoroutineScope: CoroutineScope,
private val appForegroundStateService: AppForegroundStateService,
private val networkMonitor: NetworkMonitor,
@@ -47,13 +47,15 @@ class SyncOrchestrator(
@AssistedFactory
interface Factory {
fun create(
- syncService: SyncService,
+ matrixClient: MatrixClient,
sessionCoroutineScope: CoroutineScope,
): SyncOrchestrator
}
private val tag = "SyncOrchestrator"
+ private val syncService = matrixClient.syncService
+
private val coroutineScope = sessionCoroutineScope.childScope(dispatchers.io, tag)
private val started = AtomicBoolean(false)
@@ -94,7 +96,7 @@ class SyncOrchestrator(
appForegroundStateService.isInCall,
appForegroundStateService.isSyncingNotificationEvent,
appForegroundStateService.hasRingingCall,
- appForegroundStateService.isSharingLiveLocation
+ appForegroundStateService.isSharingLiveLocation,
)
val isAppActiveFlow = combine(isAppActiveFlows) { actives -> actives.any { it } }
combine(
@@ -103,6 +105,11 @@ class SyncOrchestrator(
networkMonitor.connectivity,
isAppActiveFlow,
) { 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
Timber.tag(tag).d("isAppActive=$isAppActive, isNetworkAvailable=$isNetworkAvailable")
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/RootFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/RootFlowNodeTest.kt
new file mode 100644
index 0000000000..fea30c8019
--- /dev/null
+++ b/appnav/src/test/kotlin/io/element/android/appnav/RootFlowNodeTest.kt
@@ -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,
+ )
+ }
+}
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt
index 7309ca6ac2..32d0ec8f56 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/SyncOrchestratorTest.kt
@@ -8,10 +8,11 @@
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.test.FakeNetworkMonitor
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.services.analytics.test.FakeAnalyticsService
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
@@ -40,7 +41,7 @@ class SyncOrchestratorTest {
}
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
val syncOrchestrator = createSyncOrchestrator(
- syncService = syncService,
+ matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
)
@@ -60,7 +61,7 @@ class SyncOrchestratorTest {
}
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val syncOrchestrator = createSyncOrchestrator(
- syncService = syncService,
+ matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
)
@@ -85,7 +86,7 @@ class SyncOrchestratorTest {
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true)
val syncOrchestrator = createSyncOrchestrator(
- syncService = syncService,
+ matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -117,7 +118,7 @@ class SyncOrchestratorTest {
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Connected)
val appForegroundStateService = FakeAppForegroundStateService(initialForegroundValue = true)
val syncOrchestrator = createSyncOrchestrator(
- syncService = syncService,
+ matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -165,7 +166,7 @@ class SyncOrchestratorTest {
initialIsSyncingNotificationEventValue = false,
)
val syncOrchestrator = createSyncOrchestrator(
- syncService = syncService,
+ matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -208,7 +209,7 @@ class SyncOrchestratorTest {
initialIsSyncingNotificationEventValue = false,
)
val syncOrchestrator = createSyncOrchestrator(
- syncService = syncService,
+ matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -252,7 +253,7 @@ class SyncOrchestratorTest {
initialHasRingingCall = false,
)
val syncOrchestrator = createSyncOrchestrator(
- syncService = syncService,
+ matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -296,7 +297,7 @@ class SyncOrchestratorTest {
initialIsInCallValue = true,
)
val syncOrchestrator = createSyncOrchestrator(
- syncService = syncService,
+ matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -339,7 +340,7 @@ class SyncOrchestratorTest {
initialIsInCallValue = false,
)
val syncOrchestrator = createSyncOrchestrator(
- syncService = syncService,
+ matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
)
@@ -369,7 +370,7 @@ class SyncOrchestratorTest {
}
val networkMonitor = FakeNetworkMonitor(initialStatus = NetworkStatus.Disconnected)
val syncOrchestrator = createSyncOrchestrator(
- syncService = syncService,
+ matrixClient = FakeMatrixClient(syncService = syncService),
networkMonitor = networkMonitor,
)
@@ -381,12 +382,44 @@ class SyncOrchestratorTest {
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.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(
- syncService: FakeSyncService = FakeSyncService(),
+ matrixClient: FakeMatrixClient = FakeMatrixClient(),
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
appForegroundStateService: FakeAppForegroundStateService = FakeAppForegroundStateService(),
) = SyncOrchestrator(
- syncService = syncService,
+ matrixClient = matrixClient,
sessionCoroutineScope = backgroundScope,
networkMonitor = networkMonitor,
appForegroundStateService = appForegroundStateService,
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt
index 841f79678b..ddeae41a78 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt
@@ -45,6 +45,24 @@ class IntentResolverTest : RobolectricTest() {
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
fun `test resolve navigation intent root`() {
val sut = createIntentResolver(
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt
index cc330c2758..f85d35bbb5 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt
@@ -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.test.FakeNetworkMonitor
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.core.SessionId
import io.element.android.libraries.matrix.api.encryption.EncryptionService
@@ -131,7 +128,6 @@ class LoggedInPresenterTest {
buildMeta = buildMeta,
networkMonitor = networkMonitor,
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
- featureFlagService = FakeFeatureFlagService(),
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
).test {
encryptionService.emitRecoveryState(RecoveryState.UNKNOWN)
@@ -214,7 +210,7 @@ class LoggedInPresenterTest {
lambda.assertions()
.isCalledOnce()
// Reset the error and do not show again
- finalState.eventSink(LoggedInEvents.CloseErrorDialog(doNotShowAgain = false))
+ finalState.eventSink(LoggedInEvent.CloseErrorDialog(doNotShowAgain = false))
val lastState = awaitItem()
assertThat(lastState.pusherRegistrationState.isUninitialized()).isTrue()
assertThat(lastState.ignoreRegistrationError).isFalse()
@@ -244,7 +240,7 @@ class LoggedInPresenterTest {
lambda.assertions()
.isCalledOnce()
// Reset the error and do not show again
- finalState.eventSink(LoggedInEvents.CloseErrorDialog(doNotShowAgain = true))
+ finalState.eventSink(LoggedInEvent.CloseErrorDialog(doNotShowAgain = true))
skipItems(1)
setIgnoreRegistrationErrorLambda.assertions()
.isCalledOnce()
@@ -300,7 +296,7 @@ class LoggedInPresenterTest {
).test {
val initialState = awaitItem()
assertThat(initialState.forceNativeSlidingSyncMigration).isFalse()
- initialState.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
+ initialState.eventSink(LoggedInEvent.CheckSlidingSyncProxyAvailability)
assertThat(awaitItem().forceNativeSlidingSyncMigration).isTrue()
}
}
@@ -322,7 +318,7 @@ class LoggedInPresenterTest {
).test {
val initialState = awaitItem()
- initialState.eventSink(LoggedInEvents.LogoutAndMigrateToNativeSlidingSync)
+ initialState.eventSink(LoggedInEvent.LogoutAndMigrateToNativeSlidingSync)
advanceUntilIdle()
@@ -362,9 +358,6 @@ class LoggedInPresenterTest {
}
createLoggedInPresenter(
matrixClient = matrixClient,
- featureFlagService = FakeFeatureFlagService(
- initialState = mapOf(FeatureFlags.UserStatus.key to true),
- ),
).test {
cancelAndConsumeRemainingEvents()
assert(enableAutomaticCallStatusLambda)
@@ -374,7 +367,7 @@ class LoggedInPresenterTest {
}
@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 { }
val matrixClient = FakeMatrixClient(
accountManagementUrlResult = { Result.success(null) },
@@ -382,23 +375,13 @@ class LoggedInPresenterTest {
).apply {
isUserStatusSupportedResult = Result.success(true)
}
- val featureFlagService = FakeFeatureFlagService(
- initialState = mapOf(FeatureFlags.UserStatus.key to false),
- )
createLoggedInPresenter(
matrixClient = matrixClient,
- featureFlagService = featureFlagService,
).test {
- awaitItem()
- // Turning the flag on at runtime enables it.
- featureFlagService.setFeatureEnabled(FeatureFlags.UserStatus, true)
cancelAndConsumeRemainingEvents()
assert(enableAutomaticCallStatusLambda)
- .isCalledExactly(2)
- .withSequence(
- listOf(value(false)),
- listOf(value(true)),
- )
+ .isCalledOnce()
+ .with(value(true))
}
}
@@ -418,7 +401,6 @@ class LoggedInPresenterTest {
),
buildMeta: BuildMeta = aBuildMeta(),
networkMonitor: FakeNetworkMonitor = FakeNetworkMonitor(),
- featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
): LoggedInPresenter {
return LoggedInPresenter(
matrixClient = matrixClient,
@@ -430,7 +412,6 @@ class LoggedInPresenterTest {
buildMeta = buildMeta,
networkMonitor = networkMonitor,
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
- featureFlagService = featureFlagService,
permissionsPresenterFactory = FakePermissionsPresenterFactory(),
)
}
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/session/FakeSyncOrchestratorFactory.kt b/appnav/src/test/kotlin/io/element/android/appnav/session/FakeSyncOrchestratorFactory.kt
new file mode 100644
index 0000000000..586c8f0804
--- /dev/null
+++ b/appnav/src/test/kotlin/io/element/android/appnav/session/FakeSyncOrchestratorFactory.kt
@@ -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()
+}
diff --git a/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixSessionCacheTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/session/MatrixSessionCacheTest.kt
similarity index 97%
rename from appnav/src/test/kotlin/io/element/android/appnav/di/MatrixSessionCacheTest.kt
rename to appnav/src/test/kotlin/io/element/android/appnav/session/MatrixSessionCacheTest.kt
index 36fa471dd0..bf315ddb01 100644
--- a/appnav/src/test/kotlin/io/element/android/appnav/di/MatrixSessionCacheTest.kt
+++ b/appnav/src/test/kotlin/io/element/android/appnav/session/MatrixSessionCacheTest.kt
@@ -6,12 +6,12 @@
* 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.google.common.truth.Truth.assertThat
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.FakeMatrixClient
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
@@ -138,11 +138,11 @@ class MatrixSessionCacheTest {
return object : SyncOrchestrator.Factory {
override fun create(
- syncService: SyncService,
+ matrixClient: MatrixClient,
sessionCoroutineScope: CoroutineScope,
): SyncOrchestrator {
return SyncOrchestrator(
- syncService = syncService,
+ matrixClient = matrixClient,
sessionCoroutineScope = sessionCoroutineScope,
appForegroundStateService = FakeAppForegroundStateService(),
networkMonitor = FakeNetworkMonitor(),
diff --git a/build.gradle.kts b/build.gradle.kts
index 0e0df9dbc6..6bbc568c66 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -98,7 +98,7 @@ allprojects {
// This is disabled by default, but the CI will enforce this.
// You can override by passing `-PallWarningsAsErrors=true` in the command line
// 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
// freeCompilerArgs.addAll(listOf("-P", "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"))
diff --git a/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessor.kt b/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessor.kt
index 8b537a53fe..3e6c05ad01 100644
--- a/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessor.kt
+++ b/codegen/src/main/kotlin/io/element/android/codegen/ContributesNodeProcessor.kt
@@ -61,32 +61,32 @@ class ContributesNodeProcessor(
if (config.enableLogging) {
logger.warn("Processing ${ksClass.qualifiedName?.asString()}")
}
- generateModule(ksClass)
+ generateBindingContainer(ksClass)
generateFactory(ksClass)
}
return invalidSymbols
}
- private fun generateModule(ksClass: KSClassDeclaration) {
+ private fun generateBindingContainer(ksClass: KSClassDeclaration) {
val annotation = ksClass.annotations.find { it.shortName.asString() == "ContributesNode" }!!
val scope = annotation.arguments.find { it.name?.asString() == "scope" }!!.value as KSType
- val modulePackage = ksClass.packageName.asString()
- val moduleClassName = "${ksClass.simpleName.asString()}_Module"
+ val bindingContainerPackage = ksClass.packageName.asString()
+ val bindingContainerClassName = "${ksClass.simpleName.asString()}BindingContainer"
val nodeClassName = ClassName.bestGuess(ksClass.qualifiedName!!.asString())
val content = FileSpec.builder(
- packageName = modulePackage,
- fileName = moduleClassName,
+ packageName = bindingContainerPackage,
+ fileName = bindingContainerClassName,
)
.addType(
- TypeSpec.interfaceBuilder(moduleClassName)
+ TypeSpec.interfaceBuilder(bindingContainerClassName)
.addAnnotation(AnnotationSpec.builder(Origin::class).addMember(CLASS_PLACEHOLDER, nodeClassName).build())
.addAnnotation(BindingContainer::class)
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember(CLASS_PLACEHOLDER, scope.toTypeName()).build())
.addFunction(
- FunSpec.builder("bind${ksClass.simpleName.asString()}Factory")
+ FunSpec.builder("bind${ksClass.simpleName.asString()}AssistedFactory")
.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))
.addAnnotation(Binds::class)
.addAnnotation(IntoMap::class)
@@ -114,7 +114,7 @@ class ContributesNodeProcessor(
@OptIn(KspExperimental::class)
private fun generateFactory(ksClass: KSClassDeclaration) {
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 assistedParameters = constructor.parameters.filter { it.isAnnotationPresent(Assisted::class) }
if (assistedParameters.size != 2) {
diff --git a/docs/_developer_onboarding.md b/docs/_developer_onboarding.md
index dbac9107c4..01ad915126 100644
--- a/docs/_developer_onboarding.md
+++ b/docs/_developer_onboarding.md
@@ -322,10 +322,10 @@ Example for a new feature called RoomDetails:
4. Verify that the structure looks ok and click on Finish
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).
-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`.
Fill the text field with the base name of the classes: the generated files are `Event.kt`, `State.kt`,
- `StateProvider.kt`, `Presenter.kt`, `Node.kt` and `View.kt`. For instance
+ `StatePreviewParam.kt`, `Presenter.kt`, `Node.kt` and `View.kt`. For instance
`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).
- 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
- 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
- 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).
**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
diff --git a/docs/oauth.md b/docs/oauth.md
index 1080c64b0e..8cce5abc91 100644
--- a/docs/oauth.md
+++ b/docs/oauth.md
@@ -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
-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)
clientName: InfoPlistReader.main.bundleDisplayName,
diff --git a/enterprise b/enterprise
index 3bf0df1f25..e633a343b1 160000
--- a/enterprise
+++ b/enterprise
@@ -1 +1 @@
-Subproject commit 3bf0df1f25c4df8ee6cc226b00d9b6a675b37a09
+Subproject commit e633a343b12088b1fe3fd6b27ec319c1f7f2931c
diff --git a/fastlane/metadata/android/en-US/changelogs/202608020.txt b/fastlane/metadata/android/en-US/changelogs/202608020.txt
new file mode 100644
index 0000000000..a4b397f1bb
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/202608020.txt
@@ -0,0 +1,2 @@
+Main changes in this version: bug fixes and improvements.
+Full changelog: https://github.com/element-hq/element-x-android/releases
\ No newline at end of file
diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/AnalyticsOptInEvents.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/AnalyticsOptInEvent.kt
similarity index 86%
rename from features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/AnalyticsOptInEvents.kt
rename to features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/AnalyticsOptInEvent.kt
index 4181f52ba8..6095bc67da 100644
--- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/AnalyticsOptInEvents.kt
+++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/AnalyticsOptInEvent.kt
@@ -8,6 +8,6 @@
package io.element.android.features.analytics.api
-sealed interface AnalyticsOptInEvents {
- data class EnableAnalytics(val isEnabled: Boolean) : AnalyticsOptInEvents
+sealed interface AnalyticsOptInEvent {
+ data class EnableAnalytics(val isEnabled: Boolean) : AnalyticsOptInEvent
}
diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesState.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesState.kt
index 20a180446c..b5f049460d 100644
--- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesState.kt
+++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesState.kt
@@ -8,11 +8,11 @@
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(
val applicationName: String,
val isEnabled: Boolean,
val policyUrl: String,
- val eventSink: (AnalyticsOptInEvents) -> Unit,
+ val eventSink: (AnalyticsOptInEvent) -> Unit,
)
diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStatePreviewParam.kt
similarity index 89%
rename from features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt
rename to features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStatePreviewParam.kt
index 02e07a86a2..89d19ebac8 100644
--- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt
+++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStatePreviewParam.kt
@@ -10,7 +10,7 @@ package io.element.android.features.analytics.api.preferences
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-open class AnalyticsPreferencesStateProvider : PreviewParameterProvider {
+open class AnalyticsPreferencesStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aAnalyticsPreferencesState().copy(isEnabled = true),
diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt
index 1158b57673..31891a0162 100644
--- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt
+++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt
@@ -13,7 +13,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
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.libraries.designsystem.components.LINK_TAG
import io.element.android.libraries.designsystem.components.list.ListItemContent
@@ -30,7 +30,7 @@ fun AnalyticsPreferencesView(
modifier: Modifier = Modifier,
) {
fun onEnabledChanged(isEnabled: Boolean) {
- state.eventSink(AnalyticsOptInEvents.EnableAnalytics(isEnabled = isEnabled))
+ state.eventSink(AnalyticsOptInEvent.EnableAnalytics(isEnabled = isEnabled))
}
val supportingText = stringResource(
@@ -66,7 +66,7 @@ fun AnalyticsPreferencesView(
@PreviewsDayNight
@Composable
-internal fun AnalyticsPreferencesViewPreview(@PreviewParameter(AnalyticsPreferencesStateProvider::class) state: AnalyticsPreferencesState) =
+internal fun AnalyticsPreferencesViewPreview(@PreviewParameter(AnalyticsPreferencesStatePreviewParam::class) state: AnalyticsPreferencesState) =
ElementPreview {
AnalyticsPreferencesView(
state = state,
diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenter.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenter.kt
index d7bdb85e35..05d2c2fefd 100644
--- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenter.kt
+++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenter.kt
@@ -12,7 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
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.core.meta.BuildMeta
import io.element.android.services.analytics.api.AnalyticsService
@@ -28,9 +28,9 @@ class AnalyticsOptInPresenter(
override fun present(): AnalyticsOptInState {
val localCoroutineScope = rememberCoroutineScope()
- fun handleEvent(event: AnalyticsOptInEvents) {
+ fun handleEvent(event: AnalyticsOptInEvent) {
when (event) {
- is AnalyticsOptInEvents.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
+ is AnalyticsOptInEvent.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
}
localCoroutineScope.launch {
analyticsService.setDidAskUserConsent()
diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInState.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInState.kt
index b2ebd37e20..063ed316c8 100644
--- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInState.kt
+++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInState.kt
@@ -8,10 +8,10 @@
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(
val applicationName: String,
val hasPolicyLink: Boolean,
- val eventSink: (AnalyticsOptInEvents) -> Unit
+ val eventSink: (AnalyticsOptInEvent) -> Unit
)
diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStatePreviewParam.kt
similarity index 88%
rename from features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt
rename to features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStatePreviewParam.kt
index 30a396c178..960abb7216 100644
--- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStateProvider.kt
+++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInStatePreviewParam.kt
@@ -10,7 +10,7 @@ package io.element.android.features.analytics.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-open class AnalyticsOptInStateProvider : PreviewParameterProvider {
+open class AnalyticsOptInStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aAnalyticsOptInState(),
diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt
index c05edd825c..3ead26ea48 100644
--- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt
+++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt
@@ -28,7 +28,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.appconfig.AnalyticsConfig
import io.element.android.compound.theme.ElementTheme
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.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
@@ -55,11 +55,11 @@ fun AnalyticsOptInView(
val eventSink = state.eventSink
fun onAcceptTerms() {
- eventSink(AnalyticsOptInEvents.EnableAnalytics(true))
+ eventSink(AnalyticsOptInEvent.EnableAnalytics(true))
}
fun onDeclineTerms() {
- eventSink(AnalyticsOptInEvents.EnableAnalytics(false))
+ eventSink(AnalyticsOptInEvent.EnableAnalytics(false))
}
BackHandler(onBack = ::onDeclineTerms)
@@ -171,7 +171,7 @@ private fun AnalyticsOptInFooter(
@PreviewsDayNight
@Composable
-internal fun AnalyticsOptInViewPreview(@PreviewParameter(AnalyticsOptInStateProvider::class) state: AnalyticsOptInState) = ElementPreview {
+internal fun AnalyticsOptInViewPreview(@PreviewParameter(AnalyticsOptInStatePreviewParam::class) state: AnalyticsOptInState) = ElementPreview {
AnalyticsOptInView(
state = state,
onClickTerms = {},
diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/di/AnalyticsModule.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/di/AnalyticsBindingContainer.kt
similarity index 95%
rename from features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/di/AnalyticsModule.kt
rename to features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/di/AnalyticsBindingContainer.kt
index 32a3d5826a..a4c2c68670 100644
--- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/di/AnalyticsModule.kt
+++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/di/AnalyticsBindingContainer.kt
@@ -18,7 +18,7 @@ import io.element.android.libraries.architecture.Presenter
@ContributesTo(AppScope::class)
@BindingContainer
-interface AnalyticsModule {
+interface AnalyticsBindingContainer {
@Binds
fun bindAnalyticsPreferencesPresenter(presenter: AnalyticsPreferencesPresenter): Presenter
}
diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenter.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenter.kt
index 110cb98d41..ca3388ee3c 100644
--- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenter.kt
+++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenter.kt
@@ -13,7 +13,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
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.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
@@ -31,9 +31,9 @@ class AnalyticsPreferencesPresenter(
val localCoroutineScope = rememberCoroutineScope()
val isEnabled = analyticsService.userConsentFlow.collectAsState(initial = false)
- fun handleEvent(event: AnalyticsOptInEvents) {
+ fun handleEvent(event: AnalyticsOptInEvent) {
when (event) {
- is AnalyticsOptInEvents.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
+ is AnalyticsOptInEvent.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
}
}
diff --git a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt
index 6521c7f597..f2093dfa72 100644
--- a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt
+++ b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInPresenterTest.kt
@@ -12,7 +12,7 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
-import io.element.android.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.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
@@ -37,7 +37,7 @@ class AnalyticsOptInPresenterTest {
}.test {
val initialState = awaitItem()
assertThat(analyticsService.didAskUserConsentFlow.first()).isFalse()
- initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(true))
+ initialState.eventSink.invoke(AnalyticsOptInEvent.EnableAnalytics(true))
assertThat(analyticsService.didAskUserConsentFlow.first()).isTrue()
assertThat(analyticsService.userConsentFlow.first()).isTrue()
}
@@ -55,7 +55,7 @@ class AnalyticsOptInPresenterTest {
}.test {
val initialState = awaitItem()
assertThat(analyticsService.didAskUserConsentFlow.first()).isFalse()
- initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(false))
+ initialState.eventSink.invoke(AnalyticsOptInEvent.EnableAnalytics(false))
assertThat(analyticsService.didAskUserConsentFlow.first()).isTrue()
assertThat(analyticsService.userConsentFlow.first()).isFalse()
}
diff --git a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt
index 174935a1ad..29f7d9af03 100644
--- a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt
+++ b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt
@@ -13,7 +13,7 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
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.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
@@ -67,9 +67,9 @@ class AnalyticsPreferencesPresenterTest {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.isEnabled).isTrue()
- initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(false))
+ initialState.eventSink.invoke(AnalyticsOptInEvent.EnableAnalytics(false))
assertThat(awaitItem().isEnabled).isFalse()
- initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(true))
+ initialState.eventSink.invoke(AnalyticsOptInEvent.EnableAnalytics(true))
assertThat(awaitItem().isEnabled).isTrue()
}
}
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementStateProvider.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementStatePreviewParam.kt
similarity index 90%
rename from features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementStateProvider.kt
rename to features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementStatePreviewParam.kt
index 2412fee167..df7afad32f 100644
--- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementStateProvider.kt
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/AnnouncementStatePreviewParam.kt
@@ -11,7 +11,7 @@ package io.element.android.features.announcement.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.announcement.api.Announcement
-open class AnnouncementStateProvider : PreviewParameterProvider {
+open class AnnouncementStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
anAnnouncementState(),
diff --git a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementView.kt b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementView.kt
index 5f7778900b..cb962f8968 100644
--- a/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementView.kt
+++ b/features/announcement/impl/src/main/kotlin/io/element/android/features/announcement/impl/fullscreen/FullscreenAnnouncementView.kt
@@ -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.impl.AnnouncementEvent
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.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
@@ -216,7 +216,7 @@ private fun Announcement.Fullscreen.notice(): String? = when (this) {
@PreviewsDayNight
@Composable
-internal fun FullscreenAnnouncementViewPreview(@PreviewParameter(AnnouncementStateProvider::class) state: AnnouncementState) = ElementPreview {
+internal fun FullscreenAnnouncementViewPreview(@PreviewParameter(AnnouncementStatePreviewParam::class) state: AnnouncementState) = ElementPreview {
FullscreenAnnouncementView(
state = state,
)
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvent.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvent.kt
index 1af13e148e..2053ff5102 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvent.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPictureEvent.kt
@@ -11,7 +11,9 @@ package io.element.android.features.call.impl.pip
import io.element.android.features.call.impl.utils.PipController
sealed interface PictureInPictureEvent {
+ data object OnCallStarted : PictureInPictureEvent
data class SetPipController(val pipController: PipController) : PictureInPictureEvent
data object EnterPictureInPicture : PictureInPictureEvent
data class OnPictureInPictureModeChanged(val isInPip: Boolean) : PictureInPictureEvent
+ data class SetPipOrientation(val orientation: Int?) : PictureInPictureEvent
}
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt
index 2dc16d7f26..274a80cc26 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PictureInPicturePresenter.kt
@@ -38,6 +38,9 @@ class PictureInPicturePresenter(
fun handleEvent(event: PictureInPictureEvent) {
when (event) {
+ is PictureInPictureEvent.OnCallStarted -> {
+ pipController?.setupPipCallbacks()
+ }
is PictureInPictureEvent.SetPipController -> {
pipController = event.pipController
}
@@ -55,6 +58,12 @@ class PictureInPicturePresenter(
pipController?.exitPip()
}
}
+ is PictureInPictureEvent.SetPipOrientation -> {
+ Timber.tag(loggerTag.value).d("onOrientationChange: ${event.orientation}")
+ if (isPipSupported) {
+ pipView?.setPipOrientation(event.orientation)
+ }
+ }
}
}
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipView.kt
index 90f74a37fd..54b8d77601 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipView.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/pip/PipView.kt
@@ -8,8 +8,15 @@
package io.element.android.features.call.impl.pip
+import android.os.Build
+import androidx.annotation.RequiresApi
+
interface PipView {
+ @RequiresApi(Build.VERSION_CODES.O)
+ fun setPipOrientation(orientation: Int?)
+ @RequiresApi(Build.VERSION_CODES.O)
fun setPipParams()
+ @RequiresApi(Build.VERSION_CODES.O)
fun enterPipMode(): Boolean
fun hangUp()
}
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallNotificationDataProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallNotificationDataPreviewParam.kt
similarity index 94%
rename from features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallNotificationDataProvider.kt
rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallNotificationDataPreviewParam.kt
index 9e551b3e1b..8ca3893f2b 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallNotificationDataProvider.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallNotificationDataPreviewParam.kt
@@ -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.UserId
-open class CallNotificationDataProvider : PreviewParameterProvider {
+open class CallNotificationDataPreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aCallNotificationData(
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStatePreviewParam.kt
similarity index 93%
rename from features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt
rename to features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStatePreviewParam.kt
index 155c5d3380..cc21197a51 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStateProvider.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenStatePreviewParam.kt
@@ -11,7 +11,7 @@ package io.element.android.features.call.impl.ui
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncData
-open class CallScreenStateProvider : PreviewParameterProvider {
+open class CallScreenStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aCallScreenState(),
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt
index 2537eb739c..33c3024f83 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenView.kt
@@ -118,6 +118,7 @@ internal fun CallScreenView(
if (webViewAudioManager?.isInCallMode?.get() == false) {
Timber.d("URL $url is loaded, starting in-call audio mode")
webViewAudioManager?.onCallStarted()
+ pipState.eventSink(PictureInPictureEvent.OnCallStarted)
} else {
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 },
)
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))
},
onDestroyWebView = {
@@ -260,7 +266,7 @@ private fun WebView.dispatchEscKeyEvent() {
@PreviewsDayNight
@Composable
internal fun CallScreenViewPreview(
- @PreviewParameter(CallScreenStateProvider::class) state: CallScreenState,
+ @PreviewParameter(CallScreenStatePreviewParam::class) state: CallScreenState,
) = ElementPreview {
CallScreenView(
state = state,
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt
index 9a093ff5a6..4a6b8f1c05 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt
@@ -11,9 +11,9 @@ package io.element.android.features.call.impl.ui
import android.Manifest
import android.app.PictureInPictureParams
import android.content.Intent
+import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
-import android.util.Rational
import android.view.WindowManager
import android.webkit.PermissionRequest
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.enterprise.api.EnterpriseService
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.bindings
import io.element.android.libraries.audio.api.AudioFocus
@@ -86,6 +87,8 @@ class ElementCallActivity :
private var eventSink: ((CallScreenEvent) -> Unit)? = null
+ private var currentPipOrientation: Int? = null
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -199,6 +202,9 @@ class ElementCallActivity :
}
DisposableEffect(Unit) {
val onPictureInPictureModeChangedListener = Consumer { _: PictureInPictureModeChangedInfo ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ setPipParams()
+ }
pipEventSink(PictureInPictureEvent.OnPictureInPictureModeChanged(isInPictureInPictureMode))
if (!isInPictureInPictureMode && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
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)
private fun getPictureInPictureParams(): PictureInPictureParams {
+ // Portrait for calls seems more appropriate as a fallback value
+ val orientation = currentPipOrientation ?: Configuration.ORIENTATION_PORTRAIT
return PictureInPictureParams.Builder()
- // Portrait for calls seems more appropriate
- .setAspectRatio(Rational(3, 5))
+ .setAspectRatioFromOrientation(orientation)
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
setAutoEnterEnabled(true)
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt
index 72edd59ed5..1489d9d0e7 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt
@@ -161,7 +161,7 @@ private fun ActionButton(
@PreviewsDayNight
@Composable
internal fun IncomingCallScreenPreview(
- @PreviewParameter(CallNotificationDataProvider::class) state: CallNotificationData,
+ @PreviewParameter(CallNotificationDataPreviewParam::class) state: CallNotificationData,
) = ElementPreview {
IncomingCallScreen(
notificationData = state,
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/PipController.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/PipController.kt
index b259816f9d..f7788ad520 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/PipController.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/PipController.kt
@@ -9,6 +9,7 @@
package io.element.android.features.call.impl.utils
interface PipController {
+ fun setupPipCallbacks()
suspend fun canEnterPip(): Boolean
fun enterPip()
fun exitPip()
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt
index e68e57118b..3290269971 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewAudioManager.kt
@@ -250,7 +250,7 @@ class WebViewAudioManager(
hasRegisteredCallbacks = true
}
- }
+ },
)
Timber.d("Setting androidNativeBridge javascript interface in webview")
webView.addJavascriptInterface(webViewAudioDeviceSelectedCallback, "androidNativeBridge")
diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewPipController.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewPipController.kt
index 12f8dfd00f..88648e84fa 100644
--- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewPipController.kt
+++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/WebViewPipController.kt
@@ -8,7 +8,10 @@
package io.element.android.features.call.impl.utils
+import android.content.res.Configuration
+import android.webkit.JavascriptInterface
import android.webkit.WebView
+import timber.log.Timber
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@@ -18,7 +21,18 @@ import kotlin.coroutines.suspendCoroutine
*/
class WebViewPipController(
private val webView: WebView,
+ updatePipOrientation: (orientation: Int?) -> Unit,
) : 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 {
return suspendCoroutine { continuation ->
webView.evaluateJavascript("controls.canEnterPip()") { result ->
@@ -36,3 +50,19 @@ class WebViewPipController(
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)
+ }
+}
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipController.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipController.kt
index 5153f79c31..f2eca681e9 100644
--- a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipController.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipController.kt
@@ -12,10 +12,13 @@ import io.element.android.features.call.impl.utils.PipController
import io.element.android.tests.testutils.lambda.lambdaError
class FakePipController(
+ private val setupPipCallbacksResult: () -> Unit = { lambdaError() },
private val canEnterPipResult: () -> Boolean = { lambdaError() },
private val enterPipResult: () -> Unit = { lambdaError() },
private val exitPipResult: () -> Unit = { lambdaError() },
) : PipController {
+ override fun setupPipCallbacks() = setupPipCallbacksResult()
+
override suspend fun canEnterPip(): Boolean = canEnterPipResult()
override fun enterPip() = enterPipResult()
diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipView.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipView.kt
index 55e94312bf..adfcde2c38 100644
--- a/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipView.kt
+++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/impl/pip/FakePipView.kt
@@ -13,9 +13,11 @@ import io.element.android.tests.testutils.lambda.lambdaError
class FakePipView(
private val setPipParamsResult: () -> Unit = { lambdaError() },
private val enterPipModeResult: () -> Boolean = { lambdaError() },
- private val handUpResult: () -> Unit = { lambdaError() }
+ private val handUpResult: () -> Unit = { lambdaError() },
+ private val onOrientationChangeResult: (orientation: Int?) -> Unit = { lambdaError() },
) : PipView {
override fun setPipParams() = setPipParamsResult()
override fun enterPipMode(): Boolean = enterPipModeResult()
override fun hangUp() = handUpResult()
+ override fun setPipOrientation(orientation: Int?) = onOrientationChangeResult(orientation)
}
diff --git a/features/contentscanner/impl/src/main/kotlin/io/element/android/features/contentscanner/impl/di/ContentScannerModule.kt b/features/contentscanner/impl/src/main/kotlin/io/element/android/features/contentscanner/impl/di/ContentScannerBindingContainer.kt
similarity index 97%
rename from features/contentscanner/impl/src/main/kotlin/io/element/android/features/contentscanner/impl/di/ContentScannerModule.kt
rename to features/contentscanner/impl/src/main/kotlin/io/element/android/features/contentscanner/impl/di/ContentScannerBindingContainer.kt
index 9446a603b2..8c25d8bf6a 100644
--- a/features/contentscanner/impl/src/main/kotlin/io/element/android/features/contentscanner/impl/di/ContentScannerModule.kt
+++ b/features/contentscanner/impl/src/main/kotlin/io/element/android/features/contentscanner/impl/di/ContentScannerBindingContainer.kt
@@ -21,7 +21,7 @@ import kotlinx.coroutines.CoroutineScope
@BindingContainer
@ContributesTo(RoomScope::class)
-object ContentScannerModule {
+object ContentScannerBindingContainer {
@Provides
@SingleIn(RoomScope::class)
fun providesContentScannerService(
diff --git a/features/contentscanner/impl/src/test/kotlin/io/element/android/features/contentscanner/impl/di/ContentScannerModuleTest.kt b/features/contentscanner/impl/src/test/kotlin/io/element/android/features/contentscanner/impl/di/ContentScannerBindingContainerTest.kt
similarity index 77%
rename from features/contentscanner/impl/src/test/kotlin/io/element/android/features/contentscanner/impl/di/ContentScannerModuleTest.kt
rename to features/contentscanner/impl/src/test/kotlin/io/element/android/features/contentscanner/impl/di/ContentScannerBindingContainerTest.kt
index 8c3834d696..858a21df6c 100644
--- a/features/contentscanner/impl/src/test/kotlin/io/element/android/features/contentscanner/impl/di/ContentScannerModuleTest.kt
+++ b/features/contentscanner/impl/src/test/kotlin/io/element/android/features/contentscanner/impl/di/ContentScannerBindingContainerTest.kt
@@ -15,16 +15,16 @@ import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.runTest
import org.junit.Test
-class ContentScannerModuleTest {
+class ContentScannerBindingContainerTest {
@Test
- fun `ContentScannerModule provides NoopContentScannerService when ContentScanner is null`() = runTest {
+ fun `ContentScannerBindingContainer provides NoopContentScannerService when ContentScanner is null`() = runTest {
// Given
val contentScanner: ContentScanner? = null
val sessionCoroutineScope = backgroundScope
val coroutineDispatchers = testCoroutineDispatchers()
// When
- val contentScannerService = ContentScannerModule.providesContentScannerService(
+ val contentScannerService = ContentScannerBindingContainer.providesContentScannerService(
contentScanner = contentScanner,
coroutineScope = sessionCoroutineScope,
coroutineDispatchers = coroutineDispatchers,
@@ -35,14 +35,14 @@ class ContentScannerModuleTest {
}
@Test
- fun `ContentScannerModule provides DefaultContentScannerService when ContentScanner is not null`() = runTest {
+ fun `ContentScannerBindingContainer provides DefaultContentScannerService when ContentScanner is not null`() = runTest {
// Given
val contentScanner: ContentScanner? = FakeContentScanner()
val sessionCoroutineScope = backgroundScope
val coroutineDispatchers = testCoroutineDispatchers()
// When
- val contentScannerService = ContentScannerModule.providesContentScannerService(
+ val contentScannerService = ContentScannerBindingContainer.providesContentScannerService(
contentScanner = contentScanner,
coroutineScope = sessionCoroutineScope,
coroutineDispatchers = coroutineDispatchers,
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt
index e03d36363d..b23226b9c0 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt
@@ -21,9 +21,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
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.InvitePeopleStateProvider
+import io.element.android.features.invitepeople.api.InvitePeopleStatePreviewParam
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -56,7 +56,7 @@ fun AddPeopleView(
Button(
text = stringResource(CommonStrings.action_finish),
onClick = {
- state.eventSink(InvitePeopleEvents.SendInvites)
+ state.eventSink(InvitePeopleEvent.SendInvites)
},
enabled = state.canInvite,
modifier = Modifier
@@ -86,7 +86,7 @@ private fun AddPeopleTopBar(
@PreviewsDayNight
@Composable
-internal fun AddPeopleViewPreview(@PreviewParameter(InvitePeopleStateProvider::class) state: InvitePeopleState) = ElementPreview {
+internal fun AddPeopleViewPreview(@PreviewParameter(InvitePeopleStatePreviewParam::class) state: InvitePeopleState) = ElementPreview {
AddPeopleView(
state = state,
invitePeopleView = {},
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvent.kt
similarity index 78%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt
rename to features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvent.kt
index 5de99e519d..e22e08a5b0 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvent.kt
@@ -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.ui.media.AvatarAction
-sealed interface ConfigureRoomEvents {
- data class RoomNameChanged(val name: String) : ConfigureRoomEvents
- data class TopicChanged(val topic: String) : ConfigureRoomEvents
- data class JoinRuleChanged(val joinRuleItem: JoinRuleItem) : ConfigureRoomEvents
- data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvents
- data object CreateRoom : ConfigureRoomEvents
- data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
- data class SetParentSpace(val space: SpaceRoom?) : ConfigureRoomEvents
- data object CancelCreateRoom : ConfigureRoomEvents
+sealed interface ConfigureRoomEvent {
+ data class RoomNameChanged(val name: String) : ConfigureRoomEvent
+ data class TopicChanged(val topic: String) : ConfigureRoomEvent
+ data class JoinRuleChanged(val joinRuleItem: JoinRuleItem) : ConfigureRoomEvent
+ data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvent
+ data object CreateRoom : ConfigureRoomEvent
+ data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvent
+ data class SetParentSpace(val space: SpaceRoom?) : ConfigureRoomEvent
+ data object CancelCreateRoom : ConfigureRoomEvent
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt
index abedc2bd49..849647a9d3 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt
@@ -188,14 +188,14 @@ class ConfigureRoomPresenter(
localCoroutineScope.createRoom(config, createRoomAction)
}
- fun handleEvent(event: ConfigureRoomEvents) {
+ fun handleEvent(event: ConfigureRoomEvent) {
when (event) {
- is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
- is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
- is ConfigureRoomEvents.JoinRuleChanged -> dataStore.setJoinRule(event.joinRuleItem)
- is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
- is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig)
- is ConfigureRoomEvents.HandleAvatarAction -> {
+ is ConfigureRoomEvent.RoomNameChanged -> dataStore.setRoomName(event.name)
+ is ConfigureRoomEvent.TopicChanged -> dataStore.setTopic(event.topic)
+ is ConfigureRoomEvent.JoinRuleChanged -> dataStore.setJoinRule(event.joinRuleItem)
+ is ConfigureRoomEvent.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
+ is ConfigureRoomEvent.CreateRoom -> createRoom(createRoomConfig)
+ is ConfigureRoomEvent.HandleAvatarAction -> {
when (event.action) {
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
AvatarAction.TakePhoto -> if (cameraPermissionState.permissionGranted) {
@@ -207,10 +207,10 @@ class ConfigureRoomPresenter(
AvatarAction.Remove -> dataStore.setAvatarUri(uri = null)
}
}
- is ConfigureRoomEvents.SetParentSpace -> {
+ is ConfigureRoomEvent.SetParentSpace -> {
dataStore.setParentSpace(event.space, false)
}
- ConfigureRoomEvents.CancelCreateRoom -> {
+ ConfigureRoomEvent.CancelCreateRoom -> {
createRoomAction.value = AsyncAction.Uninitialized
}
}
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt
index 0da931fa51..15f549d28f 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt
@@ -26,7 +26,7 @@ data class ConfigureRoomState(
val homeserverName: String,
val availableJoinRules: ImmutableList,
val spaces: ImmutableList,
- val eventSink: (ConfigureRoomEvents) -> Unit
+ val eventSink: (ConfigureRoomEvent) -> Unit
) {
val isValid: Boolean = config.roomName?.isNotEmpty() == true &&
(config.visibilityState is RoomVisibilityState.Private || roomAddressValidity == RoomAddressValidity.Valid) &&
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStatePreviewParam.kt
similarity index 98%
rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt
rename to features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStatePreviewParam.kt
index 9c2ad76a9b..725cfc2309 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStatePreviewParam.kt
@@ -21,7 +21,7 @@ import io.element.android.libraries.permissions.api.aPermissionsState
import io.element.android.libraries.previewutils.room.aSpaceRoom
import kotlinx.collections.immutable.toImmutableList
-open class ConfigureRoomStateProvider : PreviewParameterProvider {
+open class ConfigureRoomStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aConfigureRoomState(),
@@ -143,7 +143,7 @@ fun aConfigureRoomState(
)
},
spaces: List = emptyList(),
- eventSink: (ConfigureRoomEvents) -> Unit = { },
+ eventSink: (ConfigureRoomEvent) -> Unit = { },
) = ConfigureRoomState(
config = config,
isSpace = isSpace,
diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt
index c929060e13..fec820ffbb 100644
--- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt
+++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt
@@ -96,7 +96,7 @@ fun ConfigureRoomView(
onBackClick = onBackClick,
onNextClick = {
focusManager.clearFocus()
- state.eventSink(ConfigureRoomEvents.CreateRoom)
+ state.eventSink(ConfigureRoomEvent.CreateRoom)
},
)
}
@@ -114,20 +114,20 @@ fun ConfigureRoomView(
avatarUri = state.config.avatarUri,
roomName = state.config.roomName.orEmpty(),
onAvatarClick = ::onAvatarClick,
- onChangeRoomName = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
+ onChangeRoomName = { state.eventSink(ConfigureRoomEvent.RoomNameChanged(it)) },
)
Spacer(modifier = Modifier.height(16.dp))
RoomTopic(
modifier = Modifier.padding(horizontal = 16.dp),
topic = state.config.topic.orEmpty(),
- onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
+ onTopicChange = { state.eventSink(ConfigureRoomEvent.TopicChanged(it)) },
)
Spacer(modifier = Modifier.height(16.dp))
if (!state.isSpace && state.spaces.isNotEmpty()) {
SelectParentSpaceOptions(
spaces = state.spaces,
selectedSpace = state.config.parentSpace,
- onSelectSpace = { state.eventSink(ConfigureRoomEvents.SetParentSpace(it)) },
+ onSelectSpace = { state.eventSink(ConfigureRoomEvent.SetParentSpace(it)) },
)
}
RoomJoinRuleOptions(
@@ -136,7 +136,7 @@ fun ConfigureRoomView(
parentSpace = state.config.parentSpace,
onOptionClick = {
focusManager.clearFocus()
- state.eventSink(ConfigureRoomEvents.JoinRuleChanged(it))
+ state.eventSink(ConfigureRoomEvent.JoinRuleChanged(it))
},
)
if (state.config.visibilityState !is RoomVisibilityState.Private) {
@@ -146,7 +146,7 @@ fun ConfigureRoomView(
address = state.config.visibilityState.roomAddress().getOrNull().orEmpty(),
homeserverName = state.homeserverName,
addressValidity = state.roomAddressValidity,
- onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
+ onAddressChange = { state.eventSink(ConfigureRoomEvent.RoomAddressChanged(it)) },
label = null,
supportingText = stringResource(R.string.screen_create_room_room_address_section_footer),
)
@@ -158,7 +158,7 @@ fun ConfigureRoomView(
actions = state.avatarActions,
isVisible = isAvatarActionsSheetVisible.value,
onDismiss = { isAvatarActionsSheetVisible.value = false },
- onSelectAction = { state.eventSink(ConfigureRoomEvents.HandleAvatarAction(it)) }
+ onSelectAction = { state.eventSink(ConfigureRoomEvent.HandleAvatarAction(it)) }
)
AsyncActionView(
@@ -170,8 +170,8 @@ fun ConfigureRoomView(
},
onSuccess = { onCreateRoomSuccess(it) },
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) },
- onErrorDismiss = { state.eventSink(ConfigureRoomEvents.CancelCreateRoom) },
+ onRetry = { state.eventSink(ConfigureRoomEvent.CreateRoom) },
+ onErrorDismiss = { state.eventSink(ConfigureRoomEvent.CancelCreateRoom) },
)
PermissionsView(
@@ -349,12 +349,12 @@ private fun RoomJoinRuleOptions(
@PreviewWithLargeHeight
@Composable
-internal fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) =
+internal fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStatePreviewParam::class) state: ConfigureRoomState) =
ElementPreviewLight { ContentToPreview(state) }
@PreviewWithLargeHeight
@Composable
-internal fun ConfigureRoomViewDarkPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) =
+internal fun ConfigureRoomViewDarkPreview(@PreviewParameter(ConfigureRoomStatePreviewParam::class) state: ConfigureRoomState) =
ElementPreviewDark { ContentToPreview(state) }
@ExcludeFromCoverage
diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/ConfigureRoomPresenterTest.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/ConfigureRoomPresenterTest.kt
index 046d00a5c6..09afd38f90 100644
--- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/ConfigureRoomPresenterTest.kt
+++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/ConfigureRoomPresenterTest.kt
@@ -12,7 +12,7 @@ import androidx.core.net.toUri
import app.cash.turbine.TurbineTestContext
import com.google.common.truth.Truth.assertThat
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.ConfigureRoomState
import io.element.android.features.createroom.impl.configureroom.CreateRoomConfig
@@ -106,14 +106,14 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
assertThat(initialState.isValid).isFalse()
// Room name not empty
- initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
+ initialState.eventSink(ConfigureRoomEvent.RoomNameChanged(A_ROOM_NAME))
var newState: ConfigureRoomState = awaitItem()
config = config.copy(roomName = A_ROOM_NAME)
assertThat(newState.config).isEqualTo(config)
assertThat(newState.isValid).isTrue()
// Clear room name
- newState.eventSink(ConfigureRoomEvents.RoomNameChanged(""))
+ newState.eventSink(ConfigureRoomEvent.RoomNameChanged(""))
newState = awaitItem()
config = config.copy(roomName = null)
assertThat(newState.config).isEqualTo(config)
@@ -136,13 +136,13 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
var expectedConfig = CreateRoomConfig()
assertThat(initialState.config).isEqualTo(expectedConfig)
// Room name
- initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
+ initialState.eventSink(ConfigureRoomEvent.RoomNameChanged(A_ROOM_NAME))
var newState = awaitItem()
expectedConfig = expectedConfig.copy(roomName = A_ROOM_NAME)
assertThat(newState.config).isEqualTo(expectedConfig)
// Room topic
- newState.eventSink(ConfigureRoomEvents.TopicChanged(A_MESSAGE))
+ newState.eventSink(ConfigureRoomEvent.TopicChanged(A_MESSAGE))
newState = awaitItem()
expectedConfig = expectedConfig.copy(topic = A_MESSAGE)
assertThat(newState.config).isEqualTo(expectedConfig)
@@ -153,7 +153,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
// From gallery
val uriFromGallery = AN_URI_FROM_GALLERY
pickerProvider.givenResult(uriFromGallery.toUri())
- newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
+ newState.eventSink(ConfigureRoomEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
newState = awaitItem()
expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery)
assertThat(newState.config).isEqualTo(expectedConfig)
@@ -161,7 +161,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
val uriFromCamera = AN_URI_FROM_CAMERA
pickerProvider.givenResult(uriFromCamera.toUri())
assertThat(newState.cameraPermissionState.permissionGranted).isFalse()
- newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
+ newState.eventSink(ConfigureRoomEvent.HandleAvatarAction(AvatarAction.TakePhoto))
newState = awaitItem()
assertThat(newState.cameraPermissionState.showDialog).isTrue()
permissionsPresenter.setPermissionGranted()
@@ -173,18 +173,18 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
// Do it again, no permission is requested
val uriFromCamera2 = AN_URI_FROM_CAMERA_2
pickerProvider.givenResult(uriFromCamera2.toUri())
- newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
+ newState.eventSink(ConfigureRoomEvent.HandleAvatarAction(AvatarAction.TakePhoto))
newState = awaitItem()
expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera2)
assertThat(newState.config).isEqualTo(expectedConfig)
// Remove
- newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.Remove))
+ newState.eventSink(ConfigureRoomEvent.HandleAvatarAction(AvatarAction.Remove))
newState = awaitItem()
expectedConfig = expectedConfig.copy(avatarUri = null)
assertThat(newState.config).isEqualTo(expectedConfig)
// Room privacy
- newState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
+ newState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
newState = awaitItem()
expectedConfig = expectedConfig.copy(
visibilityState = RoomVisibilityState.Public(
@@ -208,7 +208,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
matrixClient.givenCreateRoomResult(createRoomResult)
- initialState.eventSink(ConfigureRoomEvents.CreateRoom)
+ initialState.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterCreateRoom = awaitItem()
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
val parentSpace = aSpaceRoom(joinRule = JoinRule.Public)
- initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace))
+ initialState.eventSink(ConfigureRoomEvent.SetParentSpace(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)
- initialState.eventSink(ConfigureRoomEvents.CreateRoom)
+ initialState.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterCreateRoom = awaitItem()
@@ -282,13 +282,13 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
// Use a public parent space so AskToJoin is a valid option
val parentSpace = aSpaceRoom(joinRule = JoinRule.Public)
- initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace))
+ initialState.eventSink(ConfigureRoomEvent.SetParentSpace(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)
- initialState.eventSink(ConfigureRoomEvents.CreateRoom)
+ initialState.eventSink(ConfigureRoomEvent.CreateRoom)
// We immediately receive the room power levels info needed for adding the child to a space
val powerLevels = RoomPowerLevels(
@@ -339,7 +339,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
matrixClient.givenCreateRoomResult(createRoomResult)
- initialState.eventSink(ConfigureRoomEvents.CreateRoom)
+ initialState.eventSink(ConfigureRoomEvent.CreateRoom)
skipItems(2)
val analyticsEvent = analyticsService.capturedEvents.filterIsInstance().firstOrNull()
@@ -369,14 +369,14 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
mediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(file, mockk(), mockk())))
matrixClient.givenUploadMediaResult(Result.failure(AN_EXCEPTION))
- initialState.eventSink(ConfigureRoomEvents.CreateRoom)
+ initialState.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterCreateRoom = awaitItem()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
assertThat(analyticsService.capturedEvents.filterIsInstance()).isEmpty()
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.Loading::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
@@ -399,14 +399,14 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
fakeMatrixClient.givenCreateRoomResult(createRoomResult)
// Create
- initialState.eventSink(ConfigureRoomEvents.CreateRoom)
+ initialState.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterCreateRoom = awaitItem()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
assertThat((stateAfterCreateRoom.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
// Retry
- stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom)
+ stateAfterCreateRoom.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterRetry = awaitItem()
@@ -414,7 +414,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
assertThat((stateAfterRetry.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
// Cancel
- stateAfterRetry.eventSink(ConfigureRoomEvents.CancelCreateRoom)
+ stateAfterRetry.eventSink(ConfigureRoomEvent.CancelCreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
}
@@ -430,9 +430,9 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
)
presenter.test {
val initialState = initialState()
- initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
+ initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
skipItems(1)
- initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("invalid address"))
+ initialState.eventSink(ConfigureRoomEvent.RoomAddressChanged("invalid address"))
skipItems(1)
advanceUntilIdle()
awaitItem().also { state ->
@@ -450,9 +450,9 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
)
presenter.test {
val initialState = initialState()
- initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
+ initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
skipItems(1)
- initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
+ initialState.eventSink(ConfigureRoomEvent.RoomAddressChanged("address"))
skipItems(1)
advanceUntilIdle()
awaitItem().also { state ->
@@ -470,9 +470,9 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
)
presenter.test {
val initialState = initialState()
- initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
+ initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
skipItems(1)
- initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
+ initialState.eventSink(ConfigureRoomEvent.RoomAddressChanged("address"))
skipItems(1)
advanceUntilIdle()
awaitItem().also { state ->
@@ -488,22 +488,22 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
val initialState = initialState()
// 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)
// Then check changing the parent space resets it to private
// (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
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
// 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)
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.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))
}
}
@@ -514,7 +514,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
presenter.test {
val initialState = initialState()
- initialState.eventSink(ConfigureRoomEvents.SetParentSpace(aSpaceRoom()))
+ initialState.eventSink(ConfigureRoomEvent.SetParentSpace(aSpaceRoom()))
assertThat(awaitError())
}
@@ -534,8 +534,8 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
presenter.test {
val initialState = initialState()
- initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PrivateVisibility.Private))
- initialState.eventSink(ConfigureRoomEvents.CreateRoom)
+ initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PrivateVisibility.Private))
+ initialState.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction.isLoading()).isTrue()
assertThat(awaitItem().createRoomAction.isSuccess()).isTrue()
diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationEvents.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationEvent.kt
similarity index 76%
rename from features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationEvents.kt
rename to features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationEvent.kt
index 5ceb97b232..cc1f5093fd 100644
--- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationEvents.kt
+++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationEvent.kt
@@ -8,9 +8,9 @@
package io.element.android.features.logout.impl
-sealed interface AccountDeactivationEvents {
- data class SetEraseData(val eraseData: Boolean) : AccountDeactivationEvents
- data class SetPassword(val password: String) : AccountDeactivationEvents
- data class DeactivateAccount(val isRetry: Boolean) : AccountDeactivationEvents
- data object CloseDialogs : AccountDeactivationEvents
+sealed interface AccountDeactivationEvent {
+ data class SetEraseData(val eraseData: Boolean) : AccountDeactivationEvent
+ data class SetPassword(val password: String) : AccountDeactivationEvent
+ data class DeactivateAccount(val isRetry: Boolean) : AccountDeactivationEvent
+ data object CloseDialogs : AccountDeactivationEvent
}
diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenter.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenter.kt
index eaeacd022c..11e658aa7f 100644
--- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenter.kt
+++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenter.kt
@@ -34,19 +34,19 @@ class AccountDeactivationPresenter(
val formState = remember { mutableStateOf(DeactivateFormState.Default) }
- fun handleEvent(event: AccountDeactivationEvents) {
+ fun handleEvent(event: AccountDeactivationEvent) {
when (event) {
- is AccountDeactivationEvents.SetEraseData -> {
+ is AccountDeactivationEvent.SetEraseData -> {
updateFormState(formState) {
copy(eraseData = event.eraseData)
}
}
- is AccountDeactivationEvents.SetPassword -> {
+ is AccountDeactivationEvent.SetPassword -> {
updateFormState(formState) {
copy(password = event.password)
}
}
- is AccountDeactivationEvents.DeactivateAccount ->
+ is AccountDeactivationEvent.DeactivateAccount ->
if (action.value.isConfirming() || event.isRetry) {
localCoroutineScope.deactivateAccount(
formState = formState.value,
@@ -55,7 +55,7 @@ class AccountDeactivationPresenter(
} else {
action.value = AsyncAction.ConfirmingNoParams
}
- AccountDeactivationEvents.CloseDialogs -> {
+ AccountDeactivationEvent.CloseDialogs -> {
action.value = AsyncAction.Uninitialized
}
}
diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationState.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationState.kt
index ca9751efcc..ebf9fec3c4 100644
--- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationState.kt
+++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationState.kt
@@ -15,7 +15,7 @@ import kotlinx.parcelize.Parcelize
data class AccountDeactivationState(
val deactivateFormState: DeactivateFormState,
val accountDeactivationAction: AsyncAction,
- val eventSink: (AccountDeactivationEvents) -> Unit,
+ val eventSink: (AccountDeactivationEvent) -> Unit,
) {
val submitEnabled: Boolean
get() = accountDeactivationAction is AsyncAction.Uninitialized &&
diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationStateProvider.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationStatePreviewParam.kt
similarity index 92%
rename from features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationStateProvider.kt
rename to features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationStatePreviewParam.kt
index 5c832a4d75..b5ab948b05 100644
--- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationStateProvider.kt
+++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationStatePreviewParam.kt
@@ -11,7 +11,7 @@ package io.element.android.features.logout.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
-open class AccountDeactivationStateProvider : PreviewParameterProvider {
+open class AccountDeactivationStatePreviewParam : PreviewParameterProvider {
private val filledForm = aDeactivateFormState(eraseData = true, password = "password")
override val values: Sequence
get() = sequenceOf(
@@ -45,7 +45,7 @@ internal fun aDeactivateFormState(
internal fun anAccountDeactivationState(
deactivateFormState: DeactivateFormState = aDeactivateFormState(),
accountDeactivationAction: AsyncAction = AsyncAction.Uninitialized,
- eventSink: (AccountDeactivationEvents) -> Unit = {},
+ eventSink: (AccountDeactivationEvent) -> Unit = {},
) = AccountDeactivationState(
deactivateFormState = deactivateFormState,
accountDeactivationAction = accountDeactivationAction,
diff --git a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt
index 738046b3a3..d86eae41ff 100644
--- a/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt
+++ b/features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt
@@ -102,14 +102,14 @@ fun AccountDeactivationView(
Content(
state = state,
onSubmitClick = {
- eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
+ eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
}
)
Spacer(modifier = Modifier.height(32.dp))
Buttons(
state = state,
onSubmitClick = {
- eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
+ eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
}
)
}
@@ -117,13 +117,13 @@ fun AccountDeactivationView(
AccountDeactivationActionDialog(
state.accountDeactivationAction,
onConfirmClick = {
- eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
+ eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
},
onRetryClick = {
- eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = true))
+ eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = true))
},
onDismissDialog = {
- eventSink(AccountDeactivationEvents.CloseDialogs)
+ eventSink(AccountDeactivationEvent.CloseDialogs)
},
)
}
@@ -239,7 +239,7 @@ private fun Content(
headline = stringResource(R.string.screen_deactivate_account_delete_all_messages),
value = eraseData,
onChange = {
- eventSink(AccountDeactivationEvents.SetEraseData(it))
+ eventSink(AccountDeactivationEvent.SetEraseData(it))
},
enabled = !isLoading,
)
@@ -275,7 +275,7 @@ private fun Content(
onValueChange = {
val sanitized = it.sanitize()
passwordFieldState = sanitized
- eventSink(AccountDeactivationEvents.SetPassword(sanitized))
+ eventSink(AccountDeactivationEvent.SetPassword(sanitized))
},
placeholder = stringResource(CommonStrings.common_password),
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 {
- return replace("\n", "")
+ return filterNot { it == '\n' || it == '\r' }
}
@PreviewsDayNight
@Composable
internal fun AccountDeactivationViewPreview(
- @PreviewParameter(AccountDeactivationStateProvider::class) state: AccountDeactivationState,
+ @PreviewParameter(AccountDeactivationStatePreviewParam::class) state: AccountDeactivationState,
) = ElementPreview {
AccountDeactivationView(
state,
diff --git a/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenterTest.kt b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenterTest.kt
index ee7f8e470d..d6402c2f59 100644
--- a/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenterTest.kt
+++ b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationPresenterTest.kt
@@ -47,11 +47,11 @@ class AccountDeactivationPresenterTest {
}.test {
val initialState = awaitItem()
assertThat(initialState.deactivateFormState).isEqualTo(DeactivateFormState.Default)
- initialState.eventSink(AccountDeactivationEvents.SetEraseData(true))
+ initialState.eventSink(AccountDeactivationEvent.SetEraseData(true))
val updatedState = awaitItem()
assertThat(updatedState.deactivateFormState).isEqualTo(DeactivateFormState.Default.copy(eraseData = true))
assertThat(updatedState.submitEnabled).isFalse()
- updatedState.eventSink(AccountDeactivationEvents.SetPassword("password"))
+ updatedState.eventSink(AccountDeactivationEvent.SetPassword("password"))
val updatedState2 = awaitItem()
assertThat(updatedState2.deactivateFormState).isEqualTo(DeactivateFormState(password = "password", eraseData = true))
assertThat(updatedState2.submitEnabled).isTrue()
@@ -71,12 +71,12 @@ class AccountDeactivationPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
- initialState.eventSink(AccountDeactivationEvents.SetPassword("password"))
+ initialState.eventSink(AccountDeactivationEvent.SetPassword("password"))
skipItems(1)
- initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
+ initialState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
val updatedState = awaitItem()
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
- updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
+ updatedState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
val updatedState2 = awaitItem()
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
val finalState = awaitItem()
@@ -98,20 +98,20 @@ class AccountDeactivationPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
- initialState.eventSink(AccountDeactivationEvents.SetPassword("password"))
- initialState.eventSink(AccountDeactivationEvents.SetEraseData(true))
+ initialState.eventSink(AccountDeactivationEvent.SetPassword("password"))
+ initialState.eventSink(AccountDeactivationEvent.SetEraseData(true))
skipItems(2)
- initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
+ initialState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
val updatedState = awaitItem()
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
- updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
+ updatedState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
val updatedState2 = awaitItem()
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
val finalState = awaitItem()
assertThat(finalState.accountDeactivationAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
recorder.assertions().isCalledOnce().with(value("password"), value(true))
// Retry
- finalState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = true))
+ finalState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = true))
val finalState2 = awaitItem()
assertThat(finalState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
assertThat(awaitItem().accountDeactivationAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
@@ -131,20 +131,20 @@ class AccountDeactivationPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
- initialState.eventSink(AccountDeactivationEvents.SetPassword("password"))
- initialState.eventSink(AccountDeactivationEvents.SetEraseData(true))
+ initialState.eventSink(AccountDeactivationEvent.SetPassword("password"))
+ initialState.eventSink(AccountDeactivationEvent.SetEraseData(true))
skipItems(2)
- initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
+ initialState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
val updatedState = awaitItem()
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
- updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
+ updatedState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
val updatedState2 = awaitItem()
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
val finalState = awaitItem()
assertThat(finalState.accountDeactivationAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
recorder.assertions().isCalledOnce().with(value("password"), value(true))
// Cancel
- finalState.eventSink(AccountDeactivationEvents.CloseDialogs)
+ finalState.eventSink(AccountDeactivationEvent.CloseDialogs)
val finalState2 = awaitItem()
assertThat(finalState2.accountDeactivationAction).isEqualTo(AsyncAction.Uninitialized)
}
diff --git a/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt
index caf4c0384f..5c650fea9d 100644
--- a/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt
+++ b/features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt
@@ -35,7 +35,7 @@ import org.robolectric.annotation.Config
class AccountDeactivationViewTest : RobolectricTest() {
@Test
fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder(expectEvents = false)
+ val eventsRecorder = EventsRecorder(expectEvents = false)
ensureCalledOnce {
setAccountDeactivationView(
state = anAccountDeactivationState(eventSink = eventsRecorder),
@@ -48,7 +48,7 @@ class AccountDeactivationViewTest : RobolectricTest() {
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on Deactivate emits the expected Event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
@@ -58,12 +58,12 @@ class AccountDeactivationViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_delete)
- eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false))
+ eventsRecorder.assertSingle(AccountDeactivationEvent.DeactivateAccount(false))
}
@Test
fun `clicking on Deactivate on the confirmation dialog emits the expected Event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
@@ -74,12 +74,12 @@ class AccountDeactivationViewTest : RobolectricTest() {
),
)
pressTag(TestTags.dialogPositive.value)
- eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false))
+ eventsRecorder.assertSingle(AccountDeactivationEvent.DeactivateAccount(false))
}
@Test
fun `clicking on retry on the confirmation dialog emits the expected Event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
@@ -90,24 +90,24 @@ class AccountDeactivationViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_retry)
- eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(true))
+ eventsRecorder.assertSingle(AccountDeactivationEvent.DeactivateAccount(true))
}
@Test
fun `switching on the erase all switch emits the expected Event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setAccountDeactivationView(
state = anAccountDeactivationState(
eventSink = eventsRecorder,
),
)
clickOn(R.string.screen_deactivate_account_delete_all_messages)
- eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(true))
+ eventsRecorder.assertSingle(AccountDeactivationEvent.SetEraseData(true))
}
@Test
fun `switching off the erase all switch emits the expected Event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
@@ -117,13 +117,13 @@ class AccountDeactivationViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_deactivate_account_delete_all_messages)
- eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(false))
+ eventsRecorder.assertSingle(AccountDeactivationEvent.SetEraseData(false))
}
@Config(qualifiers = "h1024dp")
@Test
fun `typing text in the password field emits the expected Event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
@@ -133,7 +133,7 @@ class AccountDeactivationViewTest : RobolectricTest() {
),
)
onNodeWithTag(TestTags.loginPassword.value).performTextInput("A")
- eventsRecorder.assertSingle(AccountDeactivationEvents.SetPassword("A$A_PASSWORD"))
+ eventsRecorder.assertSingle(AccountDeactivationEvent.SetPassword("A$A_PASSWORD"))
}
}
diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/ClientBuilderEnterpriseHook.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/ClientBuilderEnterpriseHook.kt
new file mode 100644
index 0000000000..bfb9fb538a
--- /dev/null
+++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/ClientBuilderEnterpriseHook.kt
@@ -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
+}
diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/ClientEnterpriseHook.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/ClientEnterpriseHook.kt
new file mode 100644
index 0000000000..e6d5c9aac1
--- /dev/null
+++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/ClientEnterpriseHook.kt
@@ -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)
+}
diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt
index 2f1cbe1187..2cb4a356be 100644
--- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt
+++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt
@@ -10,7 +10,7 @@ package io.element.android.features.enterprise.api
import androidx.compose.ui.graphics.Color
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 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.
*
* @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.
*/
- 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.
@@ -53,9 +52,9 @@ interface EnterpriseService {
/**
* 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.
@@ -99,14 +98,6 @@ interface EnterpriseService {
*/
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 {
const val ANY_ACCOUNT_PROVIDER = "*"
}
diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/remoteconfig/RemoteEnterpriseConfigResult.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/remoteconfig/RemoteEnterpriseConfigResult.kt
index 5f687ff052..0cdcdebe0b 100644
--- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/remoteconfig/RemoteEnterpriseConfigResult.kt
+++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/remoteconfig/RemoteEnterpriseConfigResult.kt
@@ -32,13 +32,13 @@ sealed interface RemoteEnterpriseConfigResult {
is Success -> data
is Outdated -> data
is Error -> null
- NotFound -> null
+ is NotFound -> null
}
fun upToDateDataOrNull(): T? = when (this) {
is Success -> data
is Outdated -> null
is Error -> null
- NotFound -> null
+ is NotFound -> null
}
}
diff --git a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultClientBuilderEnterpriseHook.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultClientBuilderEnterpriseHook.kt
new file mode 100644
index 0000000000..ee22e4399d
--- /dev/null
+++ b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultClientBuilderEnterpriseHook.kt
@@ -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
+ }
+}
diff --git a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultClientEnterpriseHook.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultClientEnterpriseHook.kt
new file mode 100644
index 0000000000..b0c0152d94
--- /dev/null
+++ b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultClientEnterpriseHook.kt
@@ -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
+ }
+}
diff --git a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt
index d07c0db569..bb10550369 100644
--- a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt
+++ b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt
@@ -14,7 +14,7 @@ import dev.zacsweers.metro.ContributesBinding
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.enterprise.api.BugReportUrl
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 kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
@@ -22,10 +22,10 @@ import kotlinx.coroutines.flow.flowOf
@ContributesBinding(AppScope::class)
class DefaultEnterpriseService : EnterpriseService {
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 = emptyList()
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
@@ -45,6 +45,4 @@ class DefaultEnterpriseService : EnterpriseService {
}
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null
-
- override fun essConfigEndpointUrl(domain: String): String? = null
}
diff --git a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/scanner/DefaultContentScannerUrlProvider.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/scanner/DefaultContentScannerUrlProvider.kt
deleted file mode 100644
index 4d4a800de9..0000000000
--- a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/scanner/DefaultContentScannerUrlProvider.kt
+++ /dev/null
@@ -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 = Result.success(null)
-
- @ContributesBinding(AppScope::class)
- class Factory : ContentScannerUrlProvider.Factory {
- override fun create(urlContentFetcher: UrlContentFetcher): ContentScannerUrlProvider {
- return DefaultContentScannerUrlProvider()
- }
- }
-}
diff --git a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt
index e346e5934a..a8efcb5873 100644
--- a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt
+++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt
@@ -12,7 +12,7 @@ import androidx.compose.ui.graphics.Color
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.enterprise.api.BugReportUrl
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.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
@@ -30,8 +30,7 @@ class FakeEnterpriseService(
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() },
- private val tweakMasUrlResult: (String, String, UrlContentFetcher) -> String = { _, _, _ -> lambdaError() },
- private val essConfigEndpointUrlResult: (String) -> String = { lambdaError() },
+ private val tweakMasUrlResult: (String, ClientUrlContentFetcher) -> String = { _, _ -> lambdaError() },
private val isElementProEnforcedResult: (String) -> Boolean = { lambdaError() },
) : EnterpriseService {
private val brandColorState = MutableStateFlow(initialBrandColor)
@@ -41,8 +40,8 @@ class FakeEnterpriseService(
isEnterpriseUserResult(sessionId)
}
- override suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher): String = simulateLongTask {
- tweakMasUrlResult(url, homeserver, urlContentFetcher)
+ override suspend fun tweakMasUrl(url: String, urlContentFetcher: ClientUrlContentFetcher): String = simulateLongTask {
+ tweakMasUrlResult(url, urlContentFetcher)
}
override fun homeserverAllowList(): List {
@@ -53,8 +52,8 @@ class FakeEnterpriseService(
isAllowedToConnectToHomeserverResult(homeserverUrl)
}
- override suspend fun isElementProEnforced(homeserverUrl: String): Boolean = simulateLongTask {
- isElementProEnforcedResult(homeserverUrl)
+ override suspend fun isElementProEnforced(serverName: String): Boolean = simulateLongTask {
+ isElementProEnforcedResult(serverName)
}
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = simulateLongTask {
@@ -85,8 +84,4 @@ class FakeEnterpriseService(
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? {
return getNoisyNotificationChannelIdResult(sessionId)
}
-
- override fun essConfigEndpointUrl(domain: String): String {
- return essConfigEndpointUrlResult(domain)
- }
}
diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesEvents.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesEvent.kt
similarity index 76%
rename from features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesEvents.kt
rename to features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesEvent.kt
index 0161a5d657..9fab72424f 100644
--- a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesEvents.kt
+++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesEvent.kt
@@ -8,6 +8,6 @@
package io.element.android.features.forward.impl
-sealed interface ForwardMessagesEvents {
- data object ClearError : ForwardMessagesEvents
+sealed interface ForwardMessagesEvent {
+ data object ClearError : ForwardMessagesEvent
}
diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt
index 1145f294a3..6068866653 100644
--- a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt
+++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenter.kt
@@ -48,9 +48,9 @@ class ForwardMessagesPresenter(
@Composable
override fun present(): ForwardMessagesState {
- fun handleEvent(event: ForwardMessagesEvents) {
+ fun handleEvent(event: ForwardMessagesEvent) {
when (event) {
- ForwardMessagesEvents.ClearError -> forwardingActionState.value = AsyncAction.Uninitialized
+ ForwardMessagesEvent.ClearError -> forwardingActionState.value = AsyncAction.Uninitialized
}
}
diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesState.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesState.kt
index b1e46adb03..141c1bad38 100644
--- a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesState.kt
+++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesState.kt
@@ -13,5 +13,5 @@ import io.element.android.libraries.matrix.api.core.RoomId
data class ForwardMessagesState(
val forwardAction: AsyncAction>,
- val eventSink: (ForwardMessagesEvents) -> Unit
+ val eventSink: (ForwardMessagesEvent) -> Unit
)
diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesStateProvider.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesStatePreviewParam.kt
similarity index 89%
rename from features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesStateProvider.kt
rename to features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesStatePreviewParam.kt
index bbb5b380ae..fc61925fcc 100644
--- a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesStateProvider.kt
+++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesStatePreviewParam.kt
@@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
-open class ForwardMessagesStateProvider : PreviewParameterProvider {
+open class ForwardMessagesStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aForwardMessagesState(),
@@ -32,7 +32,7 @@ open class ForwardMessagesStateProvider : PreviewParameterProvider> = AsyncAction.Uninitialized,
- eventSink: (ForwardMessagesEvents) -> Unit = {}
+ eventSink: (ForwardMessagesEvent) -> Unit = {}
) = ForwardMessagesState(
forwardAction = forwardAction,
eventSink = eventSink
diff --git a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesView.kt b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesView.kt
index 8065054b2d..8c622d8e29 100644
--- a/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesView.kt
+++ b/features/forward/impl/src/main/kotlin/io/element/android/features/forward/impl/ForwardMessagesView.kt
@@ -31,14 +31,14 @@ fun ForwardMessagesView(
stringResource(id = CommonStrings.error_unknown)
},
onErrorDismiss = {
- state.eventSink(ForwardMessagesEvents.ClearError)
+ state.eventSink(ForwardMessagesEvent.ClearError)
},
)
}
@PreviewsDayNight
@Composable
-internal fun ForwardMessagesViewPreview(@PreviewParameter(ForwardMessagesStateProvider::class) state: ForwardMessagesState) = ElementPreview {
+internal fun ForwardMessagesViewPreview(@PreviewParameter(ForwardMessagesStatePreviewParam::class) state: ForwardMessagesState) = ElementPreview {
ForwardMessagesView(
state = state,
onForwardSuccess = {}
diff --git a/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenterTest.kt b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenterTest.kt
index 5e2f74ccad..0ff3210f2e 100644
--- a/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenterTest.kt
+++ b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesPresenterTest.kt
@@ -86,7 +86,7 @@ class ForwardMessagesPresenterTest {
val failedForwardState = awaitItem()
assertThat(failedForwardState.forwardAction.isFailure()).isTrue()
// Then clear error
- failedForwardState.eventSink(ForwardMessagesEvents.ClearError)
+ failedForwardState.eventSink(ForwardMessagesEvent.ClearError)
assertThat(awaitItem().forwardAction.isUninitialized()).isTrue()
forwardEventLambda.assertions().isCalledOnce()
}
diff --git a/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesViewTest.kt b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesViewTest.kt
index a73478479e..3302e3430f 100644
--- a/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesViewTest.kt
+++ b/features/forward/impl/src/test/kotlin/io/element/android/features/forward/impl/ForwardMessagesViewTest.kt
@@ -29,7 +29,7 @@ import org.junit.Test
class ForwardMessagesViewTest : RobolectricTest() {
@Test
fun `cancel error emits the expected event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setForwardMessagesView(
aForwardMessagesState(
forwardAction = AsyncAction.Failure(AN_EXCEPTION),
@@ -37,13 +37,13 @@ class ForwardMessagesViewTest : RobolectricTest() {
),
)
pressTag(TestTags.dialogPositive.value)
- eventsRecorder.assertSingle(ForwardMessagesEvents.ClearError)
+ eventsRecorder.assertSingle(ForwardMessagesEvent.ClearError)
}
@Test
fun `success invokes onForwardSuccess`() = runAndroidComposeUiTest {
val data = listOf(A_ROOM_ID)
- val eventsRecorder = EventsRecorder(expectEvents = false)
+ val eventsRecorder = EventsRecorder(expectEvents = false)
ensureCalledOnceWithParam?>(data) { callback ->
setForwardMessagesView(
aForwardMessagesState(
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/di/FtueModule.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/di/FtueBindingContainer.kt
similarity index 82%
rename from features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/di/FtueModule.kt
rename to features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/di/FtueBindingContainer.kt
index 52f7a43bde..3ba42f687f 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/di/FtueModule.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/di/FtueBindingContainer.kt
@@ -18,7 +18,7 @@ import io.element.android.libraries.di.SessionScope
@ContributesTo(SessionScope::class)
@BindingContainer
-interface FtueModule {
+interface FtueBindingContainer {
@Binds
- fun bindChooseSelfVerificationMethodPresenter(presenter: ChooseSelfVerificationModePresenter): Presenter
+ fun bindChooseSelfVerificationModePresenter(presenter: ChooseSelfVerificationModePresenter): Presenter
}
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInEvents.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInEvent.kt
similarity index 65%
rename from features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInEvents.kt
rename to features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInEvent.kt
index e0bd5acf43..3ce3b5b2cf 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInEvents.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInEvent.kt
@@ -8,7 +8,7 @@
package io.element.android.features.ftue.impl.notifications
-sealed interface NotificationsOptInEvents {
- data object ContinueClicked : NotificationsOptInEvents
- data object NotNowClicked : NotificationsOptInEvents
+sealed interface NotificationsOptInEvent {
+ data object ContinueClicked : NotificationsOptInEvent
+ data object NotNowClicked : NotificationsOptInEvent
}
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt
index deb7fc59b9..45365670f7 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenter.kt
@@ -52,16 +52,16 @@ class NotificationsOptInPresenter(
override fun present(): NotificationsOptInState {
val notificationsPermissionsState = postNotificationPermissionsPresenter.present()
- fun handleEvent(event: NotificationsOptInEvents) {
+ fun handleEvent(event: NotificationsOptInEvent) {
when (event) {
- NotificationsOptInEvents.ContinueClicked -> {
+ NotificationsOptInEvent.ContinueClicked -> {
if (notificationsPermissionsState.permissionGranted) {
callback.onNotificationsOptInFinished()
} else {
notificationsPermissionsState.eventSink(PermissionsEvent.RequestPermissions)
}
}
- NotificationsOptInEvents.NotNowClicked -> {
+ NotificationsOptInEvent.NotNowClicked -> {
if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
appCoroutineScope.setPermissionDenied()
}
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInState.kt
index d12cec0778..cff99ab6df 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInState.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInState.kt
@@ -12,5 +12,5 @@ import io.element.android.libraries.permissions.api.PermissionsState
data class NotificationsOptInState(
val notificationsPermissionState: PermissionsState,
- val eventSink: (NotificationsOptInEvents) -> Unit
+ val eventSink: (NotificationsOptInEvent) -> Unit
)
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInStateProvider.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInStatePreviewParam.kt
similarity index 88%
rename from features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInStateProvider.kt
rename to features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInStatePreviewParam.kt
index 1b2e5d2d58..07569a238e 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInStateProvider.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInStatePreviewParam.kt
@@ -11,7 +11,7 @@ package io.element.android.features.ftue.impl.notifications
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.permissions.api.aPermissionsState
-open class NotificationsOptInStateProvider : PreviewParameterProvider {
+open class NotificationsOptInStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aNotificationsOptInState(),
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt
index 6955d8e501..e2db8ce4b7 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInView.kt
@@ -86,14 +86,14 @@ private fun NotificationsOptInFooter(state: NotificationsOptInState) {
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_ok),
onClick = {
- state.eventSink(NotificationsOptInEvents.ContinueClicked)
+ state.eventSink(NotificationsOptInEvent.ContinueClicked)
}
)
TextButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_not_now),
onClick = {
- state.eventSink(NotificationsOptInEvents.NotNowClicked)
+ state.eventSink(NotificationsOptInEvent.NotNowClicked)
}
)
}
@@ -178,7 +178,7 @@ private fun NotificationRow(
@PreviewsDayNight
@Composable
internal fun NotificationsOptInViewPreview(
- @PreviewParameter(NotificationsOptInStateProvider::class) state: NotificationsOptInState
+ @PreviewParameter(NotificationsOptInStatePreviewParam::class) state: NotificationsOptInState
) {
ElementPreview {
NotificationsOptInView(
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModePresenter.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModePresenter.kt
index 2e37cbe8a5..f19c323d52 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModePresenter.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModePresenter.kt
@@ -15,7 +15,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import dev.zacsweers.metro.Inject
-import io.element.android.features.logout.api.direct.DirectLogoutEvents
+import io.element.android.features.logout.api.direct.DirectLogoutEvent
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
@@ -67,7 +67,7 @@ class ChooseSelfVerificationModePresenter(
fun handleEvent(event: ChooseSelfVerificationModeEvent) {
when (event) {
- ChooseSelfVerificationModeEvent.SignOut -> directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
+ ChooseSelfVerificationModeEvent.SignOut -> directLogoutState.eventSink(DirectLogoutEvent.Logout(ignoreSdkError = false))
}
}
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStatePreviewParam.kt
similarity index 97%
rename from features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt
rename to features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStatePreviewParam.kt
index b35a9624b9..1af6786289 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStateProvider.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeStatePreviewParam.kt
@@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.libraries.architecture.AsyncData
-class ChooseSelfVerificationModeStateProvider :
+class ChooseSelfVerificationModeStatePreviewParam :
PreviewParameterProvider {
override val values = sequenceOf(
aChooseSelfVerificationModeState(
diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt
index 1bfa10daf2..96d6b82d47 100644
--- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt
+++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSelfVerificationModeView.kt
@@ -149,7 +149,7 @@ private fun ChooseSelfVerificationModeButtons(
@PreviewsDayNight
@Composable
internal fun ChooseSelfVerificationModeViewPreview(
- @PreviewParameter(ChooseSelfVerificationModeStateProvider::class) state: ChooseSelfVerificationModeState
+ @PreviewParameter(ChooseSelfVerificationModeStatePreviewParam::class) state: ChooseSelfVerificationModeState
) = ElementPreview {
ChooseSelfVerificationModeView(
state = state,
diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenterTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenterTest.kt
index 32d68dd1e1..a456d9f373 100644
--- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenterTest.kt
+++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/notifications/NotificationsOptInPresenterTest.kt
@@ -55,7 +55,7 @@ class NotificationsOptInPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
- initialState.eventSink(NotificationsOptInEvents.ContinueClicked)
+ initialState.eventSink(NotificationsOptInEvent.ContinueClicked)
assertThat(awaitItem().notificationsPermissionState.showDialog).isTrue()
}
}
@@ -70,7 +70,7 @@ class NotificationsOptInPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
- initialState.eventSink(NotificationsOptInEvents.ContinueClicked)
+ initialState.eventSink(NotificationsOptInEvent.ContinueClicked)
assertThat(isFinished).isTrue()
}
}
@@ -86,7 +86,7 @@ class NotificationsOptInPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
- initialState.eventSink(NotificationsOptInEvents.NotNowClicked)
+ initialState.eventSink(NotificationsOptInEvent.NotNowClicked)
assertThat(isFinished).isTrue()
}
}
@@ -105,7 +105,7 @@ class NotificationsOptInPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
- initialState.eventSink(NotificationsOptInEvents.NotNowClicked)
+ initialState.eventSink(NotificationsOptInEvent.NotNowClicked)
// Allow background coroutines to run
runCurrent()
diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt
index 697c51f776..8ceb683993 100644
--- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt
+++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/sessionverification/choosemode/ChooseSessionVerificationModePresenterTest.kt
@@ -9,7 +9,7 @@
package io.element.android.features.ftue.impl.sessionverification.choosemode
import com.google.common.truth.Truth.assertThat
-import io.element.android.features.logout.api.direct.DirectLogoutEvents
+import io.element.android.features.logout.api.direct.DirectLogoutEvent
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.libraries.architecture.AsyncData
@@ -112,7 +112,7 @@ class ChooseSessionVerificationModePresenterTest {
@Test
fun `sing out action triggers a direct logout`() = runTest {
- val logoutEventRecorder = lambdaRecorder {}
+ val logoutEventRecorder = lambdaRecorder {}
val logoutPresenter = Presenter {
aDirectLogoutState(eventSink = logoutEventRecorder)
}
@@ -121,7 +121,7 @@ class ChooseSessionVerificationModePresenterTest {
val initial = awaitItem()
initial.eventSink(ChooseSelfVerificationModeEvent.SignOut)
logoutEventRecorder.assertions().isCalledOnce()
- .with(value(DirectLogoutEvents.Logout(ignoreSdkError = false)))
+ .with(value(DirectLogoutEvent.Logout(ignoreSdkError = false)))
}
}
diff --git a/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt b/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt
index 1a90245611..1c6a0a906e 100644
--- a/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt
+++ b/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt
@@ -12,6 +12,7 @@ import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
+import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
@@ -23,7 +24,7 @@ interface HomeEntryPoint : FeatureEntryPoint {
): Node
interface Callback : Plugin {
- fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?)
+ fun navigateToRoom(roomId: RoomId, eventId: EventId?, joinedRoom: JoinedRoom?)
fun navigateToCreateRoom()
fun navigateToCreateSpace()
fun navigateToSettings()
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt
index 00f285e3f4..20f27f67bd 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt
@@ -56,6 +56,7 @@ import io.element.android.libraries.designsystem.utils.DelayedVisibility
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
+import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.coroutines.CoroutineScope
@@ -183,6 +184,7 @@ class HomeFlowNode(
fun navigateToRoom(
roomId: RoomId,
+ eventId: EventId?,
) {
if (!loadingJoinedRoomJob.value.isUninitialized()) {
Timber.w("Already loading a room, ignoring navigateToRoom for $roomId")
@@ -195,7 +197,7 @@ class HomeFlowNode(
}.fold(
onSuccess = { joinedRoom ->
if (isActive) {
- callback.navigateToRoom(roomId, joinedRoom)
+ callback.navigateToRoom(roomId = roomId, eventId = eventId, joinedRoom = joinedRoom)
loadingJoinedRoomJob.value = AsyncData.Success(coroutineContext.job)
// Wait a bit before resetting the state to avoid allowing to open several rooms
delay(200.milliseconds)
@@ -205,7 +207,7 @@ class HomeFlowNode(
onFailure = {
// If the operation wasn't cancelled, navigate without the room, using the room id
if (it !is CancellationException) {
- callback.navigateToRoom(roomId, null)
+ callback.navigateToRoom(roomId = roomId, eventId = null, joinedRoom = null)
}
loadingJoinedRoomJob.value = AsyncData.Failure(error = it, prevData = coroutineContext.job)
// Wait a bit before resetting the state to avoid allowing to open several rooms
@@ -233,7 +235,7 @@ class HomeFlowNode(
acceptDeclineInviteView = {
acceptDeclineInviteView.Render(
state = state.roomListState.acceptDeclineInviteState,
- onAcceptInviteSuccess = ::navigateToRoom,
+ onAcceptInviteSuccess = { navigateToRoom(roomId = it, eventId = null) },
onDeclineInviteSuccess = { },
modifier = Modifier
)
@@ -269,6 +271,11 @@ class HomeFlowNode(
parentNode = this,
buildContext = buildContext,
inviteData = navTarget.inviteData,
+ callback = object : DeclineInviteAndBlockEntryPoint.Callback {
+ override fun onDeclineSuccess() {
+ backstack.pop()
+ }
+ },
)
}
is NavTarget.SelectNewOwnersWhenLeavingRoom -> {
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStatePreviewParam.kt
similarity index 96%
rename from features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt
rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStatePreviewParam.kt
index ea9ff7a234..14ab6dc7f6 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStateProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeStatePreviewParam.kt
@@ -10,7 +10,7 @@ package io.element.android.features.home.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.home.impl.roomlist.RoomListState
-import io.element.android.features.home.impl.roomlist.RoomListStateProvider
+import io.element.android.features.home.impl.roomlist.RoomListStatePreviewParam
import io.element.android.features.home.impl.roomlist.aRoomListState
import io.element.android.features.home.impl.roomlist.aRoomsContentState
import io.element.android.features.home.impl.roomlist.generateRoomListRoomSummaryList
@@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.toImmutableList
-open class HomeStateProvider : PreviewParameterProvider {
+open class HomeStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aHomeState(),
@@ -43,7 +43,7 @@ open class HomeStateProvider : PreviewParameterProvider {
aHomeState(
currentHomeNavigationBarItem = HomeNavigationBarItem.Spaces,
),
- ) + RoomListStateProvider().values.map {
+ ) + RoomListStatePreviewParam().values.map {
aHomeState(roomListState = it)
} + aHomeState(
currentHomeNavigationBarItem = HomeNavigationBarItem.Spaces,
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt
index f5a2400f2f..024d013871 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeView.kt
@@ -55,6 +55,8 @@ import io.element.android.features.home.impl.roomlist.RoomListContextMenu
import io.element.android.features.home.impl.roomlist.RoomListDeclineInviteMenu
import io.element.android.features.home.impl.roomlist.RoomListEvent
import io.element.android.features.home.impl.roomlist.RoomListState
+import io.element.android.features.home.impl.search.GlobalSearchEvent
+import io.element.android.features.home.impl.search.GlobalSearchView
import io.element.android.features.home.impl.search.RoomListSearchView
import io.element.android.features.home.impl.spacefilters.SpaceFiltersEvent
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
@@ -73,6 +75,7 @@ import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState
+import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.launch
@@ -80,7 +83,7 @@ import kotlinx.coroutines.launch
@Composable
fun HomeView(
homeState: HomeState,
- onRoomClick: (RoomId) -> Unit,
+ onRoomClick: (RoomId, EventId?) -> Unit,
onSettingsClick: () -> Unit,
onSetUpRecoveryClick: () -> Unit,
onConfirmRecoveryKeyClick: () -> Unit,
@@ -122,22 +125,34 @@ fun HomeView(
state = homeState,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
- onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) },
+ onRoomClick = { roomId -> if (firstThrottler.canHandle()) onRoomClick(roomId, null) },
onOpenSettings = { if (firstThrottler.canHandle()) onSettingsClick() },
onStartChatClick = { if (firstThrottler.canHandle()) onStartChatClick() },
onCreateSpaceClick = { if (firstThrottler.canHandle()) onCreateSpaceClick() },
onMenuActionClick = onMenuActionClick,
)
- // This overlaid view will only be visible when state.displaySearchResults is true
- RoomListSearchView(
- state = state.searchState,
- eventSink = state.eventSink,
- hideInvitesAvatars = state.hideInvitesAvatars,
- onRoomClick = { if (firstThrottler.canHandle()) onRoomClick(it) },
- modifier = Modifier
- .fillMaxSize()
- .background(ElementTheme.colors.bgCanvasDefault)
- )
+
+ if (state.globalSearchState.isEnabled) {
+ GlobalSearchView(
+ state = state.globalSearchState,
+ onSelectSearchResult = { roomId, eventId -> if (firstThrottler.canHandle()) onRoomClick(roomId, eventId) },
+ modifier = Modifier
+ .fillMaxSize()
+ .background(ElementTheme.colors.bgCanvasDefault),
+ )
+ } else {
+ // This overlaid view will only be visible when state.displaySearchResults is true
+ RoomListSearchView(
+ state = state.searchState,
+ eventSink = state.eventSink,
+ hideInvitesAvatars = state.hideInvitesAvatars,
+ onRoomClick = { roomId -> if (firstThrottler.canHandle()) onRoomClick(roomId, null) },
+ modifier = Modifier
+ .fillMaxSize()
+ .background(ElementTheme.colors.bgCanvasDefault)
+ )
+ }
+
acceptDeclineInviteView()
}
}
@@ -186,8 +201,18 @@ private fun HomeScaffold(
selectedNavigationItem = state.currentHomeNavigationBarItem,
currentUserAndNeighbors = state.currentUserAndNeighbors,
showAvatarIndicator = state.showAvatarIndicator,
- areSearchResultsDisplayed = roomListState.searchState.isSearchActive,
- onToggleSearch = { roomListState.eventSink(RoomListEvent.ToggleSearchResults) },
+ areSearchResultsDisplayed = if (roomListState.globalSearchState.isEnabled) {
+ roomListState.globalSearchState.isSearchActive
+ } else {
+ roomListState.searchState.isSearchActive
+ },
+ onToggleSearch = {
+ if (roomListState.globalSearchState.isEnabled) {
+ roomListState.globalSearchState.eventSink(GlobalSearchEvent.ToggleSearchVisibility)
+ } else {
+ roomListState.eventSink(RoomListEvent.ToggleSearchResults)
+ }
+ },
onMenuActionClick = onMenuActionClick,
onOpenSettings = onOpenSettings,
onAccountSwitch = {
@@ -346,10 +371,10 @@ internal fun RoomListRoomSummary.contentType() = displayType.ordinal
@PreviewsDayNight
@Composable
-internal fun HomeViewPreview(@PreviewParameter(HomeStateProvider::class) state: HomeState) = ElementPreview {
+internal fun HomeViewPreview(@PreviewParameter(HomeStatePreviewParam::class) state: HomeState) = ElementPreview {
HomeView(
homeState = state,
- onRoomClick = {},
+ onRoomClick = { _, _ -> },
onSettingsClick = {},
onSetUpRecoveryClick = {},
onConfirmRecoveryKeyClick = {},
@@ -369,7 +394,7 @@ internal fun HomeViewPreview(@PreviewParameter(HomeStateProvider::class) state:
internal fun HomeViewA11yPreview() = ElementPreview {
HomeView(
homeState = aHomeState(),
- onRoomClick = {},
+ onRoomClick = { _, _ -> },
onSettingsClick = {},
onSetUpRecoveryClick = {},
onConfirmRecoveryKeyClick = {},
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BatteryOptimizationBanner.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BatteryOptimizationBanner.kt
index c6a9480bc9..95826665fe 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BatteryOptimizationBanner.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/BatteryOptimizationBanner.kt
@@ -16,7 +16,7 @@ import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
-import io.element.android.libraries.push.api.battery.BatteryOptimizationEvents
+import io.element.android.libraries.push.api.battery.BatteryOptimizationEvent
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
@@ -31,8 +31,8 @@ internal fun BatteryOptimizationBanner(
description = stringResource(R.string.banner_battery_optimization_content_android),
type = AnnouncementType.Actionable(
actionText = stringResource(R.string.banner_battery_optimization_submit_android),
- onActionClick = { state.eventSink(BatteryOptimizationEvents.RequestDisableOptimizations) },
- onDismissClick = { state.eventSink(BatteryOptimizationEvents.Dismiss) },
+ onActionClick = { state.eventSink(BatteryOptimizationEvent.RequestDisableOptimizations) },
+ onDismissClick = { state.eventSink(BatteryOptimizationEvent.Dismiss) },
),
)
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/FullScreenIntentPermissionBanner.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/FullScreenIntentPermissionBanner.kt
index 92a5d8dbaa..f2826415d2 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/FullScreenIntentPermissionBanner.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/FullScreenIntentPermissionBanner.kt
@@ -16,7 +16,7 @@ import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
-import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvents
+import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvent
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
import io.element.android.libraries.ui.strings.CommonStrings
@@ -31,8 +31,8 @@ fun FullScreenIntentPermissionBanner(
description = stringResource(R.string.full_screen_intent_banner_message),
type = AnnouncementType.Actionable(
actionText = stringResource(CommonStrings.action_continue),
- onDismissClick = { state.eventSink(FullScreenIntentPermissionsEvents.Dismiss) },
- onActionClick = { state.eventSink(FullScreenIntentPermissionsEvents.OpenSettings) },
+ onDismissClick = { state.eventSink(FullScreenIntentPermissionsEvent.Dismiss) },
+ onActionClick = { state.eventSink(FullScreenIntentPermissionsEvent.OpenSettings) },
),
modifier = modifier.roomListBannerPadding(),
)
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt
index 266b6f5a59..e06fe7c46d 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomListContentView.kt
@@ -42,7 +42,7 @@ import io.element.android.features.home.impl.filters.selection.FilterSelectionSt
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
import io.element.android.features.home.impl.roomlist.RoomListContentState
-import io.element.android.features.home.impl.roomlist.RoomListContentStateProvider
+import io.element.android.features.home.impl.roomlist.RoomListContentStatePreviewParam
import io.element.android.features.home.impl.roomlist.RoomListEvent
import io.element.android.features.home.impl.roomlist.SecurityBannerState
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
@@ -336,7 +336,7 @@ private fun EmptyScaffold(
@PreviewsDayNight
@Composable
-internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStateProvider::class) state: RoomListContentState) = ElementPreview {
+internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePreviewParam::class) state: RoomListContentState) = ElementPreview {
RoomListContentView(
contentState = state,
filtersState = aRoomListFiltersState(
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt
index 2a21518e83..883d8aff76 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt
@@ -43,7 +43,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.home.impl.R
import io.element.android.features.home.impl.model.LatestEvent
import io.element.android.features.home.impl.model.RoomListRoomSummary
-import io.element.android.features.home.impl.model.RoomListRoomSummaryProvider
+import io.element.android.features.home.impl.model.RoomListRoomSummaryPreviewParam
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
import io.element.android.features.home.impl.roomlist.RoomListEvent
import io.element.android.libraries.core.extensions.orEmpty
@@ -449,7 +449,7 @@ private fun MentionIndicatorAtom() {
@PreviewsDayNight
@Composable
-internal fun RoomSummaryRowPreview(@PreviewParameter(RoomListRoomSummaryProvider::class) data: RoomListRoomSummary) = ElementPreview {
+internal fun RoomSummaryRowPreview(@PreviewParameter(RoomListRoomSummaryPreviewParam::class) data: RoomListRoomSummary) = ElementPreview {
RoomSummaryRow(
room = data,
hideInviteAvatars = false,
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/HomeSpacesModule.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/HomeSpacesBindingContainer.kt
similarity index 95%
rename from features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/HomeSpacesModule.kt
rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/HomeSpacesBindingContainer.kt
index 5e2212375d..989a558718 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/HomeSpacesModule.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/HomeSpacesBindingContainer.kt
@@ -18,7 +18,7 @@ import io.element.android.libraries.di.SessionScope
@BindingContainer
@ContributesTo(SessionScope::class)
-interface HomeSpacesModule {
+interface HomeSpacesBindingContainer {
@Binds
fun bindHomeSpacesPresenter(presenter: HomeSpacesPresenter): Presenter
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListModule.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListBindingContainer.kt
similarity index 73%
rename from features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListModule.kt
rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListBindingContainer.kt
index 1eeba4fff7..b275559206 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListModule.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/di/RoomListBindingContainer.kt
@@ -15,6 +15,8 @@ import io.element.android.features.home.impl.filters.RoomListFiltersPresenter
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.roomlist.RoomListPresenter
import io.element.android.features.home.impl.roomlist.RoomListState
+import io.element.android.features.home.impl.search.GlobalSearchPresenter
+import io.element.android.features.home.impl.search.GlobalSearchState
import io.element.android.features.home.impl.search.RoomListSearchPresenter
import io.element.android.features.home.impl.search.RoomListSearchState
import io.element.android.features.home.impl.spacefilters.SpaceFiltersPresenter
@@ -24,16 +26,19 @@ import io.element.android.libraries.di.SessionScope
@ContributesTo(SessionScope::class)
@BindingContainer
-interface RoomListModule {
+interface RoomListBindingContainer {
@Binds
fun bindRoomListPresenter(presenter: RoomListPresenter): Presenter
@Binds
- fun bindSearchPresenter(presenter: RoomListSearchPresenter): Presenter
+ fun bindRoomListSearchPresenter(presenter: RoomListSearchPresenter): Presenter
@Binds
- fun bindFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter
+ fun bindRoomListFiltersPresenter(presenter: RoomListFiltersPresenter): Presenter
@Binds
fun bindSpaceFiltersPresenter(presenter: SpaceFiltersPresenter): Presenter
+
+ @Binds
+ fun bindGlobalSearchPresenter(presenter: GlobalSearchPresenter): Presenter
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersStatePreviewParam.kt
similarity index 92%
rename from features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersStateProvider.kt
rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersStatePreviewParam.kt
index 7e095512fe..8118a65e1d 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersStateProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersStatePreviewParam.kt
@@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.home.impl.filters.selection.FilterSelectionState
import kotlinx.collections.immutable.toImmutableList
-class RoomListFiltersStateProvider : PreviewParameterProvider {
+class RoomListFiltersStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aRoomListFiltersState(),
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersView.kt
index 9931a98c4e..c671eecf45 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersView.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/filters/RoomListFiltersView.kt
@@ -212,7 +212,7 @@ private fun RoomListFilterView(
@PreviewsDayNight
@Composable
-internal fun RoomListFiltersViewPreview(@PreviewParameter(RoomListFiltersStateProvider::class) state: RoomListFiltersState) = ElementPreview {
+internal fun RoomListFiltersViewPreview(@PreviewParameter(RoomListFiltersStatePreviewParam::class) state: RoomListFiltersState) = ElementPreview {
RoomListFiltersView(
modifier = Modifier.padding(vertical = 4.dp),
state = state,
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryPreviewParam.kt
similarity index 99%
rename from features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt
rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryPreviewParam.kt
index 2e75858e3e..c04c3e446b 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryPreviewParam.kt
@@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.user.DisplayedStatus
import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.toImmutableList
-open class RoomListRoomSummaryProvider : PreviewParameterProvider {
+open class RoomListRoomSummaryPreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
listOf(
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStatePreviewParam.kt
similarity index 96%
rename from features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt
rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStatePreviewParam.kt
index 0ac2b7bf62..4ca87992f9 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStateProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContentStatePreviewParam.kt
@@ -19,7 +19,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableSet
-open class RoomListContentStateProvider : PreviewParameterProvider {
+open class RoomListContentStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aRoomsContentState(),
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt
index bfa6962e8b..6a334a94c2 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListContextMenu.kt
@@ -199,7 +199,7 @@ private fun RoomListModalBottomSheetContent(
@PreviewsDayNight
@Composable
internal fun RoomListContextMenuPreview(
- @PreviewParameter(RoomListStateContextMenuShownProvider::class) contextMenu: RoomListState.ContextMenu.Shown
+ @PreviewParameter(RoomListStateContextMenuShownPreviewParam::class) contextMenu: RoomListState.ContextMenu.Shown
) = ElementPreview(fillMaxSize = true) {
RoomListContextMenu(
contextMenu = contextMenu,
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenu.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenu.kt
index 0a7a29ebc2..022696cbb1 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenu.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListDeclineInviteMenu.kt
@@ -126,7 +126,7 @@ private fun RoomListDeclineInviteMenuContent(
@PreviewsDayNight
@Composable
internal fun RoomListDeclineInviteMenuPreview(
- @PreviewParameter(RoomListStateDeclineInviteMenuShownProvider::class) menu: RoomListState.DeclineInviteMenu.Shown,
+ @PreviewParameter(RoomListStateDeclineInviteMenuShownPreviewParam::class) menu: RoomListState.DeclineInviteMenu.Shown,
) = ElementPreview {
RoomListDeclineInviteMenu(
menu = menu,
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt
index fe09bd7cd1..546092ede6 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenter.kt
@@ -30,14 +30,15 @@ import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.home.impl.datasource.RoomListDataSource
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.filters.into
+import io.element.android.features.home.impl.search.GlobalSearchState
import io.element.android.features.home.impl.search.RoomListSearchEvent
import io.element.android.features.home.impl.search.RoomListSearchState
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.features.home.impl.spacefilters.into
import io.element.android.features.home.impl.spacefilters.selectedFilter
import io.element.android.features.invite.api.SeenInvitesStore
-import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.AcceptInvite
-import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.DeclineInvite
+import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent.AcceptInvite
+import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent.DeclineInvite
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomState
@@ -85,6 +86,7 @@ class RoomListPresenter(
private val announcementService: AnnouncementService,
private val coldStartWatcher: AnalyticsColdStartWatcher,
private val spaceFiltersPresenter: Presenter,
+ private val globalSearchPresenter: Presenter,
private val featureFlagService: FeatureFlagService,
) : Presenter {
private val encryptionService = client.encryptionService
@@ -97,6 +99,7 @@ class RoomListPresenter(
val searchState = searchPresenter.present()
val spaceFiltersState = spaceFiltersPresenter.present()
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
+ val globalSearchState = globalSearchPresenter.present()
LaunchedEffect(Unit) {
roomListDataSource.launchIn(this)
@@ -177,6 +180,7 @@ class RoomListPresenter(
leaveRoomState = leaveRoomState,
filtersState = filtersState,
searchState = searchState,
+ globalSearchState = globalSearchState,
spaceFiltersState = spaceFiltersState,
contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState,
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt
index e184a5a57f..4164c56e9d 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListState.kt
@@ -11,6 +11,7 @@ package io.element.android.features.home.impl.roomlist
import androidx.compose.runtime.Immutable
import io.element.android.features.home.impl.filters.RoomListFiltersState
import io.element.android.features.home.impl.model.RoomListRoomSummary
+import io.element.android.features.home.impl.search.GlobalSearchState
import io.element.android.features.home.impl.search.RoomListSearchState
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
@@ -27,6 +28,7 @@ data class RoomListState(
val leaveRoomState: LeaveRoomState,
val filtersState: RoomListFiltersState,
val searchState: RoomListSearchState,
+ val globalSearchState: GlobalSearchState,
val spaceFiltersState: SpaceFiltersState,
val contentState: RoomListContentState,
val acceptDeclineInviteState: AcceptDeclineInviteState,
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateContextMenuShownProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateContextMenuShownPreviewParam.kt
similarity index 90%
rename from features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateContextMenuShownProvider.kt
rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateContextMenuShownPreviewParam.kt
index eca1b63d7d..f4bd0eac2e 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateContextMenuShownProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateContextMenuShownPreviewParam.kt
@@ -11,7 +11,7 @@ package io.element.android.features.home.impl.roomlist
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.core.RoomId
-open class RoomListStateContextMenuShownProvider : PreviewParameterProvider {
+open class RoomListStateContextMenuShownPreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aContextMenuShown(hasNewContent = true),
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateDeclineInviteMenuShownProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateDeclineInviteMenuShownPreviewParam.kt
similarity index 90%
rename from features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateDeclineInviteMenuShownProvider.kt
rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateDeclineInviteMenuShownPreviewParam.kt
index 73d4785e96..c19da0595a 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateDeclineInviteMenuShownProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateDeclineInviteMenuShownPreviewParam.kt
@@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.model.aRoomListRoomSummary
-open class RoomListStateDeclineInviteMenuShownProvider : PreviewParameterProvider {
+open class RoomListStateDeclineInviteMenuShownPreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aDeclineInviteMenuShown(),
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStatePreviewParam.kt
similarity index 91%
rename from features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt
rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStatePreviewParam.kt
index 97600e0247..04f612870f 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStateProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/roomlist/RoomListStatePreviewParam.kt
@@ -16,7 +16,9 @@ import io.element.android.features.home.impl.model.RoomListRoomSummary
import io.element.android.features.home.impl.model.RoomSummaryDisplayType
import io.element.android.features.home.impl.model.aRoomListRoomSummary
import io.element.android.features.home.impl.model.anInviteSender
+import io.element.android.features.home.impl.search.GlobalSearchState
import io.element.android.features.home.impl.search.RoomListSearchState
+import io.element.android.features.home.impl.search.aGlobalSearchState
import io.element.android.features.home.impl.search.aRoomListSearchState
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.features.home.impl.spacefilters.anUnselectedSpaceFiltersState
@@ -31,7 +33,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
-open class RoomListStateProvider : PreviewParameterProvider {
+open class RoomListStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aRoomListState(),
@@ -41,7 +43,10 @@ open class RoomListStateProvider : PreviewParameterProvider {
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
aRoomListState(contentState = anEmptyContentState()),
aRoomListState(contentState = aSkeletonContentState()),
- aRoomListState(searchState = aRoomListSearchState(isSearchActive = true, query = "Test")),
+ aRoomListState(
+ searchState = aRoomListSearchState(isSearchActive = true, query = "Test"),
+ globalSearchState = aGlobalSearchState(isEnabled = false),
+ ),
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.SetUpRecovery)),
aRoomListState(contentState = aRoomsContentState(batteryOptimizationState = aBatteryOptimizationState(shouldDisplayBanner = true))),
aRoomListState(contentState = anEmptyContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
@@ -53,6 +58,7 @@ internal fun aRoomListState(
declineInviteMenu: RoomListState.DeclineInviteMenu = RoomListState.DeclineInviteMenu.Hidden,
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
searchState: RoomListSearchState = aRoomListSearchState(),
+ globalSearchState: GlobalSearchState = aGlobalSearchState(),
filtersState: RoomListFiltersState = aRoomListFiltersState(),
spaceFiltersState: SpaceFiltersState = anUnselectedSpaceFiltersState(),
contentState: RoomListContentState = aRoomsContentState(),
@@ -66,6 +72,7 @@ internal fun aRoomListState(
leaveRoomState = leaveRoomState,
filtersState = filtersState,
searchState = searchState,
+ globalSearchState = globalSearchState,
spaceFiltersState = spaceFiltersState,
contentState = contentState,
acceptDeclineInviteState = acceptDeclineInviteState,
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchEvent.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchEvent.kt
new file mode 100644
index 0000000000..5f8093b1de
--- /dev/null
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchEvent.kt
@@ -0,0 +1,15 @@
+/*
+ * 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.home.impl.search
+
+sealed interface GlobalSearchEvent {
+ object ClearQuery : GlobalSearchEvent
+ object ToggleSearchVisibility : GlobalSearchEvent
+ data class UpdateVisibleRange(val range: IntRange) : GlobalSearchEvent
+ data class UpdateTarget(val target: GlobalSearchTarget) : GlobalSearchEvent
+}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchPresenter.kt
new file mode 100644
index 0000000000..6e153344c9
--- /dev/null
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchPresenter.kt
@@ -0,0 +1,301 @@
+/*
+ * 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.home.impl.search
+
+import androidx.compose.foundation.text.input.clearText
+import androidx.compose.foundation.text.input.rememberTextFieldState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import dev.zacsweers.metro.Inject
+import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.architecture.Presenter
+import io.element.android.libraries.core.coroutine.CoroutineDispatchers
+import io.element.android.libraries.dateformatter.api.DateFormatter
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
+import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
+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.permalink.PermalinkParser
+import io.element.android.libraries.matrix.api.room.RoomInfo
+import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
+import io.element.android.libraries.matrix.api.search.MessageSearch
+import io.element.android.libraries.matrix.api.search.MessageSearchPaginationState
+import io.element.android.libraries.matrix.api.search.MessageSearchResult
+import io.element.android.libraries.matrix.api.search.MessageSearchService
+import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
+import io.element.android.libraries.matrix.api.timeline.item.event.MessageTypeWithAttachment
+import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.isMediaContent
+import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
+import io.element.android.libraries.matrix.ui.messages.toPlainText
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import kotlin.jvm.optionals.getOrElse
+import kotlin.time.Duration.Companion.milliseconds
+
+@Inject
+class GlobalSearchPresenter(
+ private val roomListSearchDataSourceFactory: RoomListSearchDataSource.Factory,
+ private val messageSearchService: MessageSearchService,
+ private val featureFlagService: FeatureFlagService,
+ private val latestEventFormatter: RoomLatestEventFormatter,
+ private val dateFormatter: DateFormatter,
+ private val fileSizeFormatter: FileSizeFormatter,
+ private val permalinkParser: PermalinkParser,
+ private val coroutineDispatchers: CoroutineDispatchers,
+ private val matrixClient: MatrixClient,
+) : Presenter {
+ @Composable
+ override fun present(): GlobalSearchState {
+ val coroutineScope = rememberCoroutineScope()
+ val isEnabled by produceState(false) {
+ featureFlagService.isFeatureEnabledFlow(FeatureFlags.MessageSearch).collectLatest { value = it }
+ }
+ val roomListSearchDataSource = remember { roomListSearchDataSourceFactory.create(coroutineScope = coroutineScope) }
+ val queryState = rememberTextFieldState()
+ var isSearchActive by remember { mutableStateOf(false) }
+ var searchResults: AsyncData by remember { mutableStateOf(AsyncData.Uninitialized) }
+ var currentTarget: GlobalSearchTarget by remember { mutableStateOf(GlobalSearchTarget.ROOMS) }
+ val currentMessageSearch: MessageSearch = remember { messageSearchService.createMessageSearch(scope = coroutineScope) }
+
+ LaunchedEffect(queryState.text) {
+ // Add a delay to avoid performing too many searches in a short period of time, which can be expensive
+ delay(200.milliseconds)
+
+ searchResults = if (queryState.text.isNotEmpty()) {
+ AsyncData.Loading(prevData = searchResults.dataOrNull())
+ } else {
+ AsyncData.Uninitialized
+ }
+
+ launch { roomListSearchDataSource.setSearchQuery(queryState.text.toString()) }
+ launch {
+ currentMessageSearch.setQuery(queryState.text.toString())
+ .onFailure { Timber.e(it, "Could not set query for message search") }
+ }
+ }
+
+ LaunchedEffect(currentTarget) {
+ if (queryState.text.isNotEmpty()) {
+ searchResults = AsyncData.Loading()
+ }
+
+ when (currentTarget) {
+ GlobalSearchTarget.ROOMS -> {
+ roomListSearchDataSource.roomSummaries.collectLatest { results ->
+ Timber.d("Room list search found ${results.size} items")
+ // The room list data source always returns some values, even if the query is empty,
+ // so we only want to update the search results if the query is not empty
+ searchResults = if (queryState.text.isNotEmpty()) {
+ AsyncData.Success(GlobalSearchResults.RoomListResults(results = results))
+ } else {
+ AsyncData.Uninitialized
+ }
+ }
+ }
+ GlobalSearchTarget.MESSAGES -> {
+ Timber.d("Message search found ${currentMessageSearch.results.value.size} items")
+ combine(
+ currentMessageSearch.results,
+ currentMessageSearch.paginationState,
+ ) { results, paginationState ->
+ Pair(results, paginationState)
+ }.collectLatest { (results, paginationState) ->
+ Timber.d("Message search found ${results.size} items, pagination state: $paginationState")
+
+ // Try processing the results in parallel to speed up the processing time, since we have to wait until we get the room info for each
+ // result, which can be slow if we have a lot of results. The original order is kept by using `awaitAll()`.
+ val mappedResults = results.map { result ->
+ async(coroutineDispatchers.computation) {
+ val roomInfo = matrixClient.getRoomInfoFlow(result.roomId).first().getOrElse { return@async null }
+ val formattedTimestamp = dateFormatter.format(
+ timestamp = result.timestamp,
+ mode = DateFormatterMode.TimeOrDate,
+ useRelative = true
+ )
+
+ if (!result.content.isMediaContent()) {
+ mapMessageContent(result, roomInfo, formattedTimestamp)
+ } else {
+ mapMediaContent(result, roomInfo, formattedTimestamp)
+ }
+ }
+ }
+ .awaitAll()
+ .mapNotNull { it }
+
+ searchResults = when {
+ queryState.text.isEmpty() -> AsyncData.Uninitialized
+ mappedResults.isNotEmpty() -> {
+ AsyncData.Success(GlobalSearchResults.MessageSearchResults(results = mappedResults.toImmutableList()))
+ }
+ paginationState is MessageSearchPaginationState.Idle && paginationState.endReached -> {
+ AsyncData.Success(GlobalSearchResults.MessageSearchResults(results = persistentListOf()))
+ }
+ paginationState is MessageSearchPaginationState.Loading -> {
+ AsyncData.Loading(prevData = searchResults.dataOrNull())
+ }
+ else -> {
+ AsyncData.Uninitialized
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fun handleEvent(event: GlobalSearchEvent) {
+ when (event) {
+ GlobalSearchEvent.ClearQuery -> {
+ queryState.clearText()
+ }
+ GlobalSearchEvent.ToggleSearchVisibility -> {
+ isSearchActive = !isSearchActive
+ queryState.clearText()
+ }
+ is GlobalSearchEvent.UpdateVisibleRange -> {
+ when (currentTarget) {
+ GlobalSearchTarget.ROOMS -> coroutineScope.launch {
+ // TODO Maybe this is not needed? We're not displaying the latest event or timestamp info so maybe we dont need to subscribe
+ // to the room info for each room in the search results.
+ roomListSearchDataSource.updateVisibleRange(event.range)
+ }
+ GlobalSearchTarget.MESSAGES -> coroutineScope.launch {
+ val currentCount = (searchResults.dataOrNull() as? GlobalSearchResults.MessageSearchResults)?.results?.size ?: 0
+ val currentPaginationState = currentMessageSearch.paginationState.value
+ val canPaginate = currentPaginationState is MessageSearchPaginationState.Idle && !currentPaginationState.endReached
+ if (event.range.last >= currentCount - 10 && canPaginate) {
+ currentMessageSearch.paginate()
+ }
+ }
+ }
+ }
+ is GlobalSearchEvent.UpdateTarget -> currentTarget = event.target
+ }
+ }
+
+ return GlobalSearchState(
+ isEnabled = isEnabled,
+ isSearchActive = isSearchActive,
+ queryState = queryState,
+ currentTarget = currentTarget,
+ results = searchResults,
+ eventSink = ::handleEvent,
+ )
+ }
+
+ private fun mapMessageContent(
+ result: MessageSearchResult,
+ roomInfo: RoomInfo,
+ formattedTimestamp: String,
+ ): MessageSearchResultItem.Message? {
+ val body = latestEventFormatter.format(
+ latestEvent = LatestEventValue.Remote(
+ timestamp = result.timestamp,
+ content = result.content,
+ senderId = result.senderId,
+ senderProfile = result.senderProfile,
+ isOwn = matrixClient.isMe(result.senderId),
+ ),
+ isDmRoom = false,
+ )
+ return MessageSearchResultItem.Message(
+ messageSearchResult = result,
+ body = body?.toString() ?: "",
+ formattedTimestamp = formattedTimestamp,
+ roomInfo = roomInfo,
+ )
+ }
+
+ private fun mapMediaContent(
+ result: MessageSearchResult,
+ roomInfo: RoomInfo,
+ formattedTimestamp: String,
+ ): MessageSearchResultItem.Media? {
+ val messageType = when (val content = result.content) {
+ is MessageContent if content.type is MessageTypeWithAttachment -> content.type as MessageTypeWithAttachment
+ else -> return null
+ }
+
+ val caption = messageType.toPlainText(permalinkParser, default = messageType.caption ?: messageType.filename)
+
+ val thumbnailType = when (messageType) {
+ is ImageMessageType -> AttachmentThumbnailType.Image
+ is VideoMessageType -> AttachmentThumbnailType.Video
+ is AudioMessageType -> AttachmentThumbnailType.Audio
+ is VoiceMessageType -> AttachmentThumbnailType.Voice
+ is FileMessageType -> AttachmentThumbnailType.File
+ is StickerMessageType -> AttachmentThumbnailType.Image
+ }
+ val thumbnailSource = when (messageType) {
+ is ImageMessageType -> messageType.info?.thumbnailSource ?: messageType.source
+ is VideoMessageType -> messageType.info?.thumbnailSource
+ is AudioMessageType -> null
+ is VoiceMessageType -> null
+ is FileMessageType -> null
+ is StickerMessageType -> messageType.info?.thumbnailSource ?: messageType.source
+ }
+ val blurhash = when (messageType) {
+ is ImageMessageType -> messageType.info?.blurhash
+ is VideoMessageType -> messageType.info?.blurhash
+ is AudioMessageType -> null
+ is VoiceMessageType -> null
+ is FileMessageType -> null
+ is StickerMessageType -> messageType.info?.blurhash
+ }
+
+ val extension = messageType.filename.split(".").takeIf { it.size > 1 }?.lastOrNull()?.uppercase()
+ val formattedSize = when (messageType) {
+ is ImageMessageType -> messageType.info?.size
+ is VideoMessageType -> messageType.info?.size
+ is AudioMessageType -> messageType.info?.size
+ is VoiceMessageType -> messageType.info?.size
+ is FileMessageType -> messageType.info?.size
+ is StickerMessageType -> messageType.info?.size
+ }
+ ?.let(fileSizeFormatter::format)
+
+ val mediaContent = MediaSearchResultContent(
+ filename = messageType.filename,
+ extension = extension,
+ caption = caption,
+ formattedSize = formattedSize,
+ thumbnailSource = thumbnailSource,
+ thumbnailType = thumbnailType,
+ blurhash = blurhash
+ )
+
+ return MessageSearchResultItem.Media(
+ messageSearchResult = result,
+ mediaContent = mediaContent,
+ formattedTimestamp = formattedTimestamp,
+ roomInfo = roomInfo,
+ )
+ }
+}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchState.kt
new file mode 100644
index 0000000000..d623fd5594
--- /dev/null
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchState.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.home.impl.search
+
+import androidx.compose.foundation.text.input.TextFieldState
+import androidx.compose.runtime.Immutable
+import io.element.android.features.home.impl.model.RoomListRoomSummary
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.media.MediaSource
+import io.element.android.libraries.matrix.api.room.RoomInfo
+import io.element.android.libraries.matrix.api.search.MessageSearchResult
+import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
+import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
+import kotlinx.collections.immutable.ImmutableList
+
+@Immutable
+data class GlobalSearchState(
+ val isEnabled: Boolean,
+ val isSearchActive: Boolean,
+ val queryState: TextFieldState,
+ val currentTarget: GlobalSearchTarget,
+ val results: AsyncData,
+ val eventSink: (GlobalSearchEvent) -> Unit
+)
+
+@Immutable
+enum class GlobalSearchTarget {
+ ROOMS,
+ MESSAGES,
+}
+
+@Immutable
+sealed interface GlobalSearchResults {
+ data class RoomListResults(val results: ImmutableList) : GlobalSearchResults
+ data class MessageSearchResults(val results: ImmutableList) : GlobalSearchResults
+
+ fun isEmpty(): Boolean = when (this) {
+ is RoomListResults -> results.isEmpty()
+ is MessageSearchResults -> results.isEmpty()
+ }
+}
+
+sealed interface MessageSearchResultItem {
+ val roomId: RoomId
+ val eventId: EventId
+ val senderId: UserId
+ val senderName: String
+ val roomInfo: RoomInfo
+ val formattedTimestamp: String
+
+ data class Message(
+ val messageSearchResult: MessageSearchResult,
+ val body: String,
+ override val roomInfo: RoomInfo,
+ override val formattedTimestamp: String,
+ ) : MessageSearchResultItem {
+ override val roomId: RoomId = messageSearchResult.roomId
+ override val eventId: EventId = messageSearchResult.eventId
+ override val senderId: UserId = messageSearchResult.senderId
+ override val senderName: String = messageSearchResult.senderProfile.getDisambiguatedDisplayName(senderId)
+ }
+
+ data class Media(
+ val messageSearchResult: MessageSearchResult,
+ val mediaContent: MediaSearchResultContent,
+ override val roomInfo: RoomInfo,
+ override val formattedTimestamp: String,
+ ) : MessageSearchResultItem {
+ override val roomId: RoomId = messageSearchResult.roomId
+ override val eventId: EventId = messageSearchResult.eventId
+ override val senderId: UserId = messageSearchResult.senderId
+ override val senderName: String = messageSearchResult.senderProfile.getDisambiguatedDisplayName(senderId)
+ }
+}
+
+data class MediaSearchResultContent(
+ val filename: String,
+ val extension: String?,
+ val caption: String?,
+ val formattedSize: String?,
+ val thumbnailSource: MediaSource?,
+ val thumbnailType: AttachmentThumbnailType,
+ val blurhash: String?,
+)
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchStatePreviewParam.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchStatePreviewParam.kt
new file mode 100644
index 0000000000..73487c5069
--- /dev/null
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchStatePreviewParam.kt
@@ -0,0 +1,249 @@
+/*
+ * 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.home.impl.search
+
+import androidx.compose.foundation.text.input.TextFieldState
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.features.home.impl.roomlist.aRoomListRoomSummaryList
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.RoomAlias
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.api.core.UserId
+import io.element.android.libraries.matrix.api.media.MediaSource
+import io.element.android.libraries.matrix.api.room.CallIntentConsensus
+import io.element.android.libraries.matrix.api.room.CurrentUserMembership
+import io.element.android.libraries.matrix.api.room.RoomInfo
+import io.element.android.libraries.matrix.api.room.RoomMember
+import io.element.android.libraries.matrix.api.room.RoomNotificationMode
+import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
+import io.element.android.libraries.matrix.api.room.join.JoinRule
+import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevels
+import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsValues
+import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom
+import io.element.android.libraries.matrix.api.search.MessageSearchResult
+import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
+import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo
+import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
+import io.element.android.libraries.matrix.api.timeline.item.event.MessageType
+import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
+import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
+import io.element.android.libraries.matrix.api.user.MatrixUser
+import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
+import io.element.android.libraries.matrix.ui.messages.reply.aProfileDetailsReady
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.persistentMapOf
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.collections.immutable.toPersistentList
+
+class GlobalSearchStatePreviewParam : PreviewParameterProvider {
+ override val values: Sequence
+ get() = sequenceOf(
+ // Not enabled, no preview
+ aGlobalSearchState(isEnabled = false),
+ aGlobalSearchState(isSearchActive = true, queryState = TextFieldState("Query")),
+ aGlobalSearchState(
+ isSearchActive = true,
+ currentTarget = GlobalSearchTarget.ROOMS,
+ queryState = TextFieldState("Query"),
+ results = AsyncData.Loading(),
+ ),
+ aGlobalSearchState(
+ isSearchActive = true,
+ currentTarget = GlobalSearchTarget.ROOMS,
+ queryState = TextFieldState("Query"),
+ results = AsyncData.Success(
+ GlobalSearchResults.RoomListResults(
+ results = aRoomListRoomSummaryList().mapIndexed { index, summary ->
+ summary.copy(name = "Room with Query #${index + 1}")
+ }.toPersistentList()
+ )
+ ),
+ ),
+ aGlobalSearchState(
+ isSearchActive = true,
+ currentTarget = GlobalSearchTarget.MESSAGES,
+ queryState = TextFieldState("Query"),
+ results = AsyncData.Success(GlobalSearchResults.MessageSearchResults(persistentListOf(
+ MessageSearchResultItem.Message(
+ messageSearchResult = aMessageSearchResult(eventId = EventId("\$eventId1:server.org")),
+ body = "A message with Query",
+ roomInfo = aRoomInfo(),
+ formattedTimestamp = "12:00",
+ ),
+ MessageSearchResultItem.Media(
+ messageSearchResult = aMessageSearchResult(eventId = EventId("\$eventId2:server.org")),
+ mediaContent = MediaSearchResultContent(
+ filename = "file.png",
+ extension = "PNG",
+ caption = "A caption containing Query",
+ formattedSize = "1 MB",
+ thumbnailSource = MediaSource("https://example.com/thumbnail.png"),
+ thumbnailType = AttachmentThumbnailType.Image,
+ blurhash = null,
+ ),
+ roomInfo = aRoomInfo(),
+ formattedTimestamp = "12:00",
+ ),
+ ))),
+ ),
+ aGlobalSearchState(
+ isSearchActive = true,
+ currentTarget = GlobalSearchTarget.MESSAGES,
+ queryState = TextFieldState("Query"),
+ results = AsyncData.Success(GlobalSearchResults.MessageSearchResults(persistentListOf())),
+ ),
+ )
+}
+
+fun aRoomInfo(
+ id: RoomId = RoomId("!roomId:server.org"),
+ name: String? = "A room",
+ rawName: String? = "A room raw name",
+ topic: String? = "A room topic",
+ avatarUrl: String? = "https://example.com/avatar.png",
+ isPublic: Boolean = true,
+ isDirect: Boolean = false,
+ isEncrypted: Boolean = false,
+ joinRule: JoinRule? = JoinRule.Public,
+ isSpace: Boolean = false,
+ successorRoom: SuccessorRoom? = null,
+ isFavorite: Boolean = false,
+ canonicalAlias: RoomAlias? = null,
+ alternativeAliases: List = emptyList(),
+ currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
+ inviter: RoomMember? = null,
+ activeMembersCount: Long = 2,
+ invitedMembersCount: Long = 1,
+ joinedMembersCount: Long = 1,
+ highlightCount: Long = 0,
+ notificationCount: Long = 0,
+ userDefinedNotificationMode: RoomNotificationMode? = null,
+ hasRoomCall: Boolean = false,
+ roomPowerLevels: RoomPowerLevels? = RoomPowerLevels(
+ values = RoomPowerLevelsValues(
+ ban = 0,
+ invite = 0,
+ kick = 0,
+ eventsDefault = 0,
+ stateDefault = 0,
+ redactEvents = 0,
+ roomName = 0,
+ roomAvatar = 0,
+ roomTopic = 0,
+ spaceChild = 0,
+ beacon = 0,
+ beaconInfo = 0
+ ),
+ users = persistentMapOf(),
+ ),
+ activeRoomCallParticipants: List = emptyList(),
+ heroes: List = emptyList(),
+ pinnedEventIds: List = emptyList(),
+ roomCreators: List = emptyList(),
+ isMarkedUnread: Boolean = false,
+ numUnreadMessages: Long = 0,
+ numUnreadNotifications: Long = 0,
+ numUnreadMentions: Long = 0,
+ historyVisibility: RoomHistoryVisibility = RoomHistoryVisibility.Joined,
+ roomVersion: String? = "11",
+ privilegedCreatorRole: Boolean = false,
+ isLowPriority: Boolean = false,
+ activeCallIntentConsensus: CallIntentConsensus = CallIntentConsensus.None,
+ isDm: Boolean = false,
+ fullyReadEventId: EventId? = null,
+) = RoomInfo(
+ id = id,
+ name = name,
+ rawName = rawName,
+ topic = topic,
+ avatarUrl = avatarUrl,
+ isPublic = isPublic,
+ isDirect = isDirect,
+ isEncrypted = isEncrypted,
+ joinRule = joinRule,
+ isSpace = isSpace,
+ successorRoom = successorRoom,
+ isFavorite = isFavorite,
+ canonicalAlias = canonicalAlias,
+ alternativeAliases = alternativeAliases.toImmutableList(),
+ currentUserMembership = currentUserMembership,
+ inviter = inviter,
+ activeMembersCount = activeMembersCount,
+ invitedMembersCount = invitedMembersCount,
+ joinedMembersCount = joinedMembersCount,
+ highlightCount = highlightCount,
+ notificationCount = notificationCount,
+ userDefinedNotificationMode = userDefinedNotificationMode,
+ hasRoomCall = hasRoomCall,
+ roomPowerLevels = roomPowerLevels,
+ activeRoomCallParticipants = activeRoomCallParticipants.toImmutableList(),
+ heroes = heroes.toImmutableList(),
+ pinnedEventIds = pinnedEventIds.toImmutableList(),
+ creators = roomCreators.toImmutableList(),
+ isMarkedUnread = isMarkedUnread,
+ numUnreadMessages = numUnreadMessages,
+ numUnreadNotifications = numUnreadNotifications,
+ numUnreadMentions = numUnreadMentions,
+ historyVisibility = historyVisibility,
+ roomVersion = roomVersion,
+ privilegedCreatorRole = privilegedCreatorRole,
+ isLowPriority = isLowPriority,
+ activeCallIntentConsensus = activeCallIntentConsensus,
+ isDm = isDm,
+ fullyReadEventId = fullyReadEventId,
+)
+
+fun aMessageContent(
+ body: String = "body",
+ inReplyTo: InReplyTo? = null,
+ isEdited: Boolean = false,
+ threadInfo: EventThreadInfo? = null,
+ messageType: MessageType = TextMessageType(
+ body = body,
+ formatted = null
+ )
+) = MessageContent(
+ body = body,
+ inReplyTo = inReplyTo,
+ isEdited = isEdited,
+ threadInfo = threadInfo,
+ type = messageType
+)
+
+internal fun aMessageSearchResult(
+ roomId: RoomId = RoomId("!roomId:server.org"),
+ eventId: EventId = EventId("\$eventId:server.org"),
+ senderId: UserId = UserId("@user:server.org"),
+ senderProfile: ProfileDetails = aProfileDetailsReady(),
+ content: MessageContent = aMessageContent(),
+ timestamp: Long = 0L,
+) = MessageSearchResult(
+ roomId = roomId,
+ eventId = eventId,
+ senderId = senderId,
+ senderProfile = senderProfile,
+ content = content,
+ timestamp = timestamp,
+)
+
+internal fun aGlobalSearchState(
+ isEnabled: Boolean = true,
+ isSearchActive: Boolean = false,
+ queryState: TextFieldState = TextFieldState(),
+ currentTarget: GlobalSearchTarget = GlobalSearchTarget.ROOMS,
+ results: AsyncData = AsyncData.Uninitialized,
+ eventSink: (GlobalSearchEvent) -> Unit = {},
+) = GlobalSearchState(
+ isEnabled = isEnabled,
+ isSearchActive = isSearchActive,
+ queryState = queryState,
+ currentTarget = currentTarget,
+ results = results,
+ eventSink = eventSink
+)
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchView.kt
new file mode 100644
index 0000000000..d5a0460f06
--- /dev/null
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/GlobalSearchView.kt
@@ -0,0 +1,564 @@
+/*
+ * 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.home.impl.search
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.input.TextFieldLineLimits
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SingleChoiceSegmentedButtonRow
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.compound.tokens.generated.CompoundIcons
+import io.element.android.features.home.impl.R
+import io.element.android.features.home.impl.model.RoomListRoomSummary
+import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
+import io.element.android.libraries.designsystem.components.BigIcon
+import io.element.android.libraries.designsystem.components.avatar.Avatar
+import io.element.android.libraries.designsystem.components.avatar.AvatarData
+import io.element.android.libraries.designsystem.components.avatar.AvatarSize
+import io.element.android.libraries.designsystem.components.avatar.AvatarType
+import io.element.android.libraries.designsystem.components.button.BackButton
+import io.element.android.libraries.designsystem.modifiers.niceClickable
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.preview.PreviewsDayNight
+import io.element.android.libraries.designsystem.text.roundToPx
+import io.element.android.libraries.designsystem.theme.components.FilledTextField
+import io.element.android.libraries.designsystem.theme.components.Icon
+import io.element.android.libraries.designsystem.theme.components.IconButton
+import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
+import io.element.android.libraries.designsystem.theme.components.Scaffold
+import io.element.android.libraries.designsystem.theme.components.SegmentedButton
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.theme.components.TopAppBar
+import io.element.android.libraries.designsystem.utils.OnVisibleRangeChangeEffect
+import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding
+import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets
+import io.element.android.libraries.matrix.api.core.EventId
+import io.element.android.libraries.matrix.api.core.RoomId
+import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail
+import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
+import io.element.android.libraries.matrix.ui.model.getAvatarData
+import io.element.android.libraries.ui.strings.CommonStrings
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
+
+@Composable
+fun GlobalSearchView(
+ state: GlobalSearchState,
+ onSelectSearchResult: (RoomId, EventId?) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ BackHandler(enabled = state.isSearchActive) {
+ state.eventSink(GlobalSearchEvent.ToggleSearchVisibility)
+ }
+
+ AnimatedVisibility(
+ visible = state.isSearchActive,
+ enter = fadeIn(),
+ exit = fadeOut(),
+ ) {
+ Column(modifier = modifier) {
+ GlobalSearchContent(
+ state = state,
+ onBackButtonClick = {
+ state.eventSink(GlobalSearchEvent.ToggleSearchVisibility)
+ },
+ onSelectSearchResult = onSelectSearchResult,
+ )
+ }
+ }
+}
+
+@Composable
+private fun GlobalSearchContent(
+ state: GlobalSearchState,
+ onBackButtonClick: () -> Unit,
+ onSelectSearchResult: (RoomId, EventId?) -> Unit,
+) {
+ val borderColor = MaterialTheme.colorScheme.tertiary
+ val strokeWidth = 1.dp
+
+ val drawBottomLineModifier = Modifier.drawBehind {
+ drawLine(
+ color = borderColor,
+ start = Offset(0f, size.height),
+ end = Offset(size.width, size.height),
+ strokeWidth = strokeWidth.value
+ )
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ navigationIcon = { BackButton(onClick = onBackButtonClick) },
+ title = {
+ // The stateSaver will keep the selection state when returning to this UI
+ val focusRequester = remember { FocusRequester() }
+ val searchLabel = stringResource(CommonStrings.action_search)
+ FilledTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .focusRequester(focusRequester)
+ .semantics { contentDescription = searchLabel },
+ state = state.queryState,
+ lineLimits = TextFieldLineLimits.SingleLine,
+ colors = TextFieldDefaults.colors(
+ focusedContainerColor = Color.Transparent,
+ unfocusedContainerColor = Color.Transparent,
+ disabledContainerColor = Color.Transparent,
+ focusedIndicatorColor = Color.Transparent,
+ unfocusedIndicatorColor = Color.Transparent,
+ disabledIndicatorColor = Color.Transparent,
+ errorIndicatorColor = Color.Transparent,
+ ),
+ trailingIcon = if (state.queryState.text.isNotEmpty()) {
+ @Composable {
+ IconButton(onClick = { state.eventSink(GlobalSearchEvent.ClearQuery) }) {
+ Icon(
+ imageVector = CompoundIcons.Close(),
+ contentDescription = stringResource(CommonStrings.a11y_clear_search_field)
+ )
+ }
+ }
+ } else {
+ null
+ },
+ )
+
+ LaunchedEffect(Unit) {
+ if (!focusRequester.restoreFocusedChild()) {
+ focusRequester.requestFocus()
+ }
+ focusRequester.saveFocusedChild()
+ }
+ }
+ )
+ },
+ contentWindowInsets = scaffoldScrollableContentInsets,
+ ) { padding ->
+ Column(
+ modifier = Modifier.padding(padding),
+ ) {
+ SingleChoiceSegmentedButtonRow(
+ modifier = drawBottomLineModifier
+ .fillMaxWidth()
+ .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp)
+ ) {
+ SegmentedButton(
+ index = 0,
+ count = 2,
+ selected = state.currentTarget == GlobalSearchTarget.ROOMS,
+ onClick = { state.eventSink(GlobalSearchEvent.UpdateTarget(GlobalSearchTarget.ROOMS)) },
+ text = stringResource(R.string.search_section_chats),
+ )
+
+ SegmentedButton(
+ index = 1,
+ count = 2,
+ selected = state.currentTarget == GlobalSearchTarget.MESSAGES,
+ onClick = { state.eventSink(GlobalSearchEvent.UpdateTarget(GlobalSearchTarget.MESSAGES)) },
+ text = stringResource(R.string.search_section_messages),
+ )
+ }
+
+ val lazyListState = rememberLazyListState()
+ OnVisibleRangeChangeEffect(lazyListState) { visibleRange ->
+ state.eventSink(GlobalSearchEvent.UpdateVisibleRange(visibleRange))
+ }
+
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = lazyColumnContentPadding,
+ state = lazyListState,
+ ) {
+ val results = state.results.dataOrNull()
+ when {
+ state.results.isUninitialized() -> startSearching()
+ state.results.isLoading() -> loading()
+ results?.isEmpty() == true -> emptySearchResults(query = state.queryState.text.toString())
+ results is GlobalSearchResults.RoomListResults -> roomListResults(
+ results = results.results,
+ onRoomClick = { roomId -> onSelectSearchResult(roomId, null) },
+ )
+ results is GlobalSearchResults.MessageSearchResults -> messageSearchResults(
+ results = results.results,
+ onSearchResultSelected = onSelectSearchResult,
+ )
+ }
+ }
+ }
+ }
+}
+
+private fun LazyListScope.startSearching() {
+ item {
+ IconTitleSubtitleMolecule(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(40.dp),
+ title = stringResource(R.string.start_searching_title),
+ subTitle = stringResource(R.string.start_searching_subtitle),
+ iconStyle = BigIcon.Style.Default(CompoundIcons.Search()),
+ )
+ }
+}
+
+private fun LazyListScope.loading() {
+ item {
+ LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
+ }
+}
+
+private fun LazyListScope.emptySearchResults(query: String) {
+ item {
+ IconTitleSubtitleMolecule(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(40.dp),
+ title = stringResource(R.string.search_no_results_title),
+ subTitle = stringResource(R.string.search_no_results_subtitle, query),
+ iconStyle = BigIcon.Style.Default(CompoundIcons.Search()),
+ )
+ }
+}
+
+private fun LazyListScope.roomListResults(
+ results: ImmutableList,
+ onRoomClick: (RoomId) -> Unit,
+) {
+ itemsIndexed(
+ items = results,
+ key = { _, roomSummary -> roomSummary.roomId },
+ ) { index, result ->
+ Row(
+ modifier = Modifier
+ .niceClickable(onClick = { onRoomClick(result.roomId) })
+ .fillMaxWidth()
+ .addSeparatorLine(color = ElementTheme.colors.separatorSecondary, isLastItem = index == results.lastIndex)
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Avatar(
+ avatarData = result.avatarData.copy(size = AvatarSize.SearchRoomListItem),
+ avatarType = AvatarType.Room(
+ heroes = result.heroes.map { it.copy(size = AvatarSize.SearchRoomListItem) }.toImmutableList(),
+ isTombstoned = result.isTombstoned,
+ ),
+ )
+
+ Column(
+ verticalArrangement = Arrangement.spacedBy(2.dp),
+ ) {
+ Text(
+ text = result.name ?: result.roomId.value,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = ElementTheme.typography.fontBodyLgRegular,
+ )
+
+ if (result.isDm) {
+ val userId = result.heroes.firstOrNull()?.id
+ userId?.let {
+ Text(
+ text = it,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = ElementTheme.typography.fontBodyMdRegular,
+ color = ElementTheme.colors.textSecondary,
+ )
+ }
+ } else {
+ result.canonicalAlias?.let {
+ Text(
+ text = it.value,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = ElementTheme.typography.fontBodyMdRegular,
+ color = ElementTheme.colors.textSecondary,
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+private fun LazyListScope.messageSearchResults(
+ results: ImmutableList,
+ onSearchResultSelected: (RoomId, EventId?) -> Unit,
+) {
+ if (results.isNotEmpty()) {
+ item {
+ Text(
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
+ text = stringResource(R.string.search_from_your_messages_header),
+ style = ElementTheme.typography.fontBodyMdMedium,
+ color = ElementTheme.colors.textSecondary,
+ )
+ }
+ }
+ itemsIndexed(
+ items = results,
+ contentType = { _, messageSearchResult -> messageSearchResult::class.java },
+ key = { _, messageSearchResult -> messageSearchResult.eventId },
+ ) { index, result ->
+ val clickableModifier = Modifier.niceClickable(onClick = { onSearchResultSelected(result.roomId, result.eventId) })
+ when (result) {
+ is MessageSearchResultItem.Message -> TextMessageSearchResultItemView(
+ modifier = clickableModifier.addSeparatorLine(color = ElementTheme.colors.separatorSecondary, isLastItem = index == results.lastIndex),
+ avatarData = result.roomInfo.getAvatarData(AvatarSize.RoomListItem),
+ avatarType = AvatarType.Room(
+ heroes = result.roomInfo.heroes.map { it.getAvatarData(AvatarSize.RoomListItem) }.toImmutableList(),
+ isTombstoned = result.roomInfo.successorRoom != null
+ ),
+ roomName = result.roomInfo.name ?: result.roomInfo.id.value,
+ body = result.body,
+ formattedTimestamp = result.formattedTimestamp,
+ )
+ is MessageSearchResultItem.Media -> {
+ MediaMessageSearchResultItemView(
+ modifier = clickableModifier.addSeparatorLine(color = ElementTheme.colors.separatorSecondary, isLastItem = index == results.lastIndex),
+ avatarData = result.roomInfo.getAvatarData(AvatarSize.RoomListItem),
+ avatarType = AvatarType.Room(
+ heroes = result.roomInfo.heroes.map { it.getAvatarData(AvatarSize.RoomListItem) }.toImmutableList(),
+ isTombstoned = result.roomInfo.successorRoom != null
+ ),
+ roomName = result.roomInfo.name ?: result.roomInfo.id.value,
+ content = result.mediaContent,
+ formattedTimestamp = result.formattedTimestamp,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun TextMessageSearchResultItemView(
+ avatarData: AvatarData,
+ avatarType: AvatarType,
+ roomName: String,
+ body: String,
+ formattedTimestamp: String,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Avatar(
+ avatarData = avatarData,
+ avatarType = avatarType,
+ )
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 12.dp, bottom = 10.dp),
+ verticalArrangement = Arrangement.spacedBy(2.dp),
+ ) {
+ Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
+ Text(
+ modifier = Modifier.weight(1f),
+ text = roomName,
+ maxLines = 1,
+ style = ElementTheme.typography.fontBodyLgMedium,
+ )
+ Text(
+ text = formattedTimestamp,
+ maxLines = 1,
+ style = ElementTheme.typography.fontBodySmRegular,
+ color = ElementTheme.colors.textSecondary,
+ )
+ }
+ Text(
+ text = body,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ style = ElementTheme.typography.fontBodyMdRegular,
+ color = ElementTheme.colors.textSecondary,
+ )
+ }
+ }
+}
+
+@Composable
+private fun MediaMessageSearchResultItemView(
+ avatarData: AvatarData,
+ avatarType: AvatarType,
+ roomName: String,
+ content: MediaSearchResultContent,
+ formattedTimestamp: String,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Avatar(
+ avatarData = avatarData,
+ avatarType = avatarType,
+ )
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 12.dp),
+ verticalArrangement = Arrangement.spacedBy(2.dp),
+ ) {
+ Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
+ Text(
+ modifier = Modifier.weight(1f),
+ text = roomName,
+ maxLines = 1,
+ style = ElementTheme.typography.fontBodyLgMedium,
+ )
+ Text(
+ text = formattedTimestamp,
+ maxLines = 1,
+ style = ElementTheme.typography.fontBodySmRegular,
+ color = ElementTheme.colors.textSecondary,
+ )
+ }
+
+ content.caption?.let {
+ Text(
+ text = it,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ style = ElementTheme.typography.fontBodyMdRegular,
+ color = ElementTheme.colors.textSecondary,
+ )
+ }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth(1f)
+ .background(ElementTheme.colors.bgSubtleSecondary, shape = RoundedCornerShape(12.dp))
+ .padding(horizontal = 12.dp, vertical = 8.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ AttachmentThumbnail(
+ modifier = Modifier
+ .size(36.dp)
+ .clip(RoundedCornerShape(4.dp)),
+ info = AttachmentThumbnailInfo(
+ type = content.thumbnailType,
+ thumbnailSource = content.thumbnailSource,
+ textContent = null,
+ blurHash = content.blurhash,
+ ),
+ thumbnailSize = 36.dp.roundToPx().toLong(),
+ backgroundColor = ElementTheme.colors.bgCanvasDefault,
+ )
+
+ Column {
+ Text(
+ text = content.filename,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = ElementTheme.typography.fontBodyLgRegular,
+ )
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ content.extension?.let {
+ Text(
+ text = it,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = ElementTheme.typography.fontBodySmRegular,
+ color = ElementTheme.colors.textSecondary,
+ )
+ }
+ content.formattedSize?.let {
+ Text(
+ text = "($it)",
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = ElementTheme.typography.fontBodySmRegular,
+ color = ElementTheme.colors.textSecondary,
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+private fun Modifier.addSeparatorLine(color: Color, isLastItem: Boolean): Modifier {
+ if (isLastItem) return this
+
+ return this.drawBehind {
+ val strokeWidthPx = 1.dp.toPx()
+ drawLine(
+ color = color,
+ start = Offset(0f, size.height - strokeWidthPx),
+ end = Offset(size.width, size.height - strokeWidthPx),
+ strokeWidth = strokeWidthPx,
+ )
+ }
+}
+
+@PreviewsDayNight
+@Composable
+internal fun GlobalSearchViewPreview(
+ @PreviewParameter(GlobalSearchStatePreviewParam::class) state: GlobalSearchState,
+) {
+ ElementPreview {
+ GlobalSearchView(
+ state = state,
+ onSelectSearchResult = { _, _ -> },
+ )
+ }
+}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt
index 03fcb5520f..df7ed0dd37 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchDataSource.kt
@@ -46,6 +46,8 @@ class RoomListSearchDataSource(
coroutineScope = coroutineScope
)
+ val loadingState = roomList.loadingState
+
val roomSummaries: Flow> = roomList.summaries
.map { roomSummaries ->
roomSummaries
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchStatePreviewParam.kt
similarity index 93%
rename from features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchStateProvider.kt
rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchStatePreviewParam.kt
index e6ec92f68b..2b857354a3 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchStateProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchStatePreviewParam.kt
@@ -15,7 +15,7 @@ import io.element.android.features.home.impl.roomlist.aRoomListRoomSummaryList
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
-class RoomListSearchStateProvider : PreviewParameterProvider {
+class RoomListSearchStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aRoomListSearchState(),
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt
index 0535ded5c5..ff354cf717 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/search/RoomListSearchView.kt
@@ -196,7 +196,7 @@ private fun RoomListSearchContent(
@PreviewsDayNight
@Composable
-internal fun RoomListSearchContentPreview(@PreviewParameter(RoomListSearchStateProvider::class) state: RoomListSearchState) = ElementPreview {
+internal fun RoomListSearchContentPreview(@PreviewParameter(RoomListSearchStatePreviewParam::class) state: RoomListSearchState) = ElementPreview {
RoomListSearchContent(
state = state,
hideInvitesAvatars = false,
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStatePreviewParam.kt
similarity index 97%
rename from features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt
rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStatePreviewParam.kt
index 264d122836..335b801908 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStateProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersStatePreviewParam.kt
@@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.api.spaces.SpaceServiceFilter
import io.element.android.libraries.previewutils.room.aSpaceRoom
import kotlinx.collections.immutable.toImmutableList
-class SpaceFiltersStateProvider : PreviewParameterProvider {
+class SpaceFiltersStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aSelectingSpaceFiltersState(),
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt
index 280f41b909..e4573f344e 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spacefilters/SpaceFiltersView.kt
@@ -189,6 +189,6 @@ private fun SpaceFilterItem(
@PreviewsDayNight
@Composable
-internal fun SpaceFiltersViewPreview(@PreviewParameter(SpaceFiltersStateProvider::class) state: SpaceFiltersState) = ElementPreview(fillMaxSize = true) {
+internal fun SpaceFiltersViewPreview(@PreviewParameter(SpaceFiltersStatePreviewParam::class) state: SpaceFiltersState) = ElementPreview(fillMaxSize = true) {
SpaceFiltersView(state = state)
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesEvents.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesEvent.kt
similarity index 89%
rename from features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesEvents.kt
rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesEvent.kt
index 88290417dd..0a32da7c09 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesEvents.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesEvent.kt
@@ -8,4 +8,4 @@
package io.element.android.features.home.impl.spaces
-sealed interface HomeSpacesEvents
+sealed interface HomeSpacesEvent
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt
index b758a1e593..2e2406be87 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesPresenter.kt
@@ -39,7 +39,7 @@ class HomeSpacesPresenter(
seenInvitesStore.seenRoomIds().map { it.toImmutableSet() }
}.collectAsState(persistentSetOf())
- fun handleEvent(event: HomeSpacesEvents) {
+ fun handleEvent(event: HomeSpacesEvent) {
// when (event) { }
}
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt
index e93f04291e..e1c8827087 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesState.kt
@@ -19,7 +19,7 @@ data class HomeSpacesState(
val seenSpaceInvites: ImmutableSet,
val hideInvitesAvatar: Boolean,
val canExploreSpaces: Boolean,
- val eventSink: (HomeSpacesEvents) -> Unit,
+ val eventSink: (HomeSpacesEvent) -> Unit,
)
sealed interface CurrentSpace {
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStatePreviewParam.kt
similarity index 91%
rename from features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt
rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStatePreviewParam.kt
index 17f2cbad31..8d4acae8b7 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStateProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesStatePreviewParam.kt
@@ -15,11 +15,11 @@ import io.element.android.libraries.previewutils.room.aSpaceRoom
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
-open class HomeSpacesStateProvider : PreviewParameterProvider {
+open class HomeSpacesStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aHomeSpacesState(
- spaceRooms = SpaceRoomProvider().values.toList(),
+ spaceRooms = SpaceRoomPreviewParam().values.toList(),
seenSpaceInvites = setOf(
RoomId("!spaceId3:example.com"),
),
@@ -43,7 +43,7 @@ internal fun aHomeSpacesState(
seenSpaceInvites: Set = emptySet(),
hideInvitesAvatar: Boolean = false,
canExploreSpaces: Boolean = true,
- eventSink: (HomeSpacesEvents) -> Unit = {},
+ eventSink: (HomeSpacesEvent) -> Unit = {},
) = HomeSpacesState(
space = space,
spaceRooms = spaceRooms.toImmutableList(),
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt
index 9125912bf0..914d08d7f6 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/HomeSpacesView.kt
@@ -171,7 +171,7 @@ private fun EmptySpaceHomeView(
@PreviewsDayNight
@Composable
internal fun HomeSpacesViewPreview(
- @PreviewParameter(HomeSpacesStateProvider::class) state: HomeSpacesState,
+ @PreviewParameter(HomeSpacesStatePreviewParam::class) state: HomeSpacesState,
) = ElementPreview {
HomeSpacesView(
state = state,
diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomPreviewParam.kt
similarity index 95%
rename from features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomProvider.kt
rename to features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomPreviewParam.kt
index 4a77e45372..25744157aa 100644
--- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomProvider.kt
+++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/spaces/SpaceRoomPreviewParam.kt
@@ -14,7 +14,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.previewutils.room.aSpaceRoom
-class SpaceRoomProvider : PreviewParameterProvider {
+class SpaceRoomPreviewParam : PreviewParameterProvider {
override val values: Sequence = sequenceOf(
aSpaceRoom(),
aSpaceRoom(
diff --git a/features/home/impl/src/main/res/values/temporary.xml b/features/home/impl/src/main/res/values/temporary.xml
new file mode 100644
index 0000000000..c44001e5a6
--- /dev/null
+++ b/features/home/impl/src/main/res/values/temporary.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ Start searching…
+ Search for chats and messages
+ Chats
+ From your messages
+ Messages
+ No results
+ There are no results for “%1$s”. Try a new search term.
+
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt
index 8eee8e1ee2..c0cb5f7c01 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt
@@ -11,6 +11,8 @@ package io.element.android.features.home.impl
import com.bumble.appyx.core.modality.BuildContext
import com.google.common.truth.Truth.assertThat
import io.element.android.features.home.api.HomeEntryPoint
+import io.element.android.features.invite.test.declineandblock.FakeDeclineInviteAndBlockEntryPoint
+import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.test.FakeMatrixClient
@@ -36,14 +38,14 @@ class DefaultHomeEntryPointTest : RobolectricTest() {
acceptDeclineInviteView = { _, _, _, _ -> lambdaError() },
directLogoutView = { _ -> lambdaError() },
reportRoomEntryPoint = { _, _, _ -> lambdaError() },
- declineInviteAndBlockUserEntryPoint = { _, _, _ -> lambdaError() },
+ declineInviteAndBlockUserEntryPoint = FakeDeclineInviteAndBlockEntryPoint(),
changeRoomMemberRolesEntryPoint = { _, _, _, _ -> lambdaError() },
leaveRoomRenderer = { _, _, _ -> lambdaError() },
sessionCoroutineScope = backgroundScope,
)
}
val callback = object : HomeEntryPoint.Callback {
- override fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) = lambdaError()
+ override fun navigateToRoom(roomId: RoomId, eventId: EventId?, joinedRoom: JoinedRoom?) = lambdaError()
override fun navigateToCreateRoom() = lambdaError()
override fun navigateToCreateSpace() = lambdaError()
override fun navigateToSettings() = lambdaError()
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt
index 64dfc46ee2..9bc8719a8b 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListPresenterTest.kt
@@ -20,11 +20,12 @@ import io.element.android.features.home.impl.filters.aRoomListFiltersState
import io.element.android.features.home.impl.model.createRoomListRoomSummary
import io.element.android.features.home.impl.search.RoomListSearchEvent
import io.element.android.features.home.impl.search.RoomListSearchState
+import io.element.android.features.home.impl.search.aGlobalSearchState
import io.element.android.features.home.impl.search.aRoomListSearchState
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.features.home.impl.spacefilters.aDisabledSpaceFiltersState
import io.element.android.features.invite.api.SeenInvitesStore
-import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
+import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
import io.element.android.features.invite.test.InMemorySeenInvitesStore
@@ -473,7 +474,7 @@ class RoomListPresenterTest {
@Test
fun `present - when a room is invited then accept and decline events are sent to acceptDeclinePresenter`() = runTest {
- val eventSinkRecorder = lambdaRecorder { _: AcceptDeclineInviteEvents -> }
+ val eventSinkRecorder = lambdaRecorder { _: AcceptDeclineInviteEvent -> }
val acceptDeclinePresenter = Presenter {
anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
}
@@ -512,8 +513,8 @@ class RoomListPresenterTest {
assert(eventSinkRecorder)
.isCalledExactly(2)
.withSequence(
- listOf(value(AcceptDeclineInviteEvents.AcceptInvite(inviteData))),
- listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false))),
+ listOf(value(AcceptDeclineInviteEvent.AcceptInvite(inviteData))),
+ listOf(value(AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false))),
)
}
}
@@ -680,6 +681,7 @@ class RoomListPresenterTest {
analyticsService = FakeAnalyticsService(),
),
searchPresenter = searchPresenter,
+ globalSearchPresenter = { aGlobalSearchState() },
filtersPresenter = filtersPresenter,
spaceFiltersPresenter = spaceFiltersPresenter,
analyticsService = analyticsService,
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt
index 773f428917..bd18cd903e 100644
--- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt
@@ -277,7 +277,7 @@ private fun AndroidComposeUiTest.setRoomListView(
setSafeContent {
HomeView(
homeState = aHomeState(roomListState = state),
- onRoomClick = onRoomClick,
+ onRoomClick = { roomId, _ -> onRoomClick(roomId) },
onSettingsClick = onSettingsClick,
onSetUpRecoveryClick = onSetUpRecoveryClick,
onConfirmRecoveryKeyClick = onConfirmRecoveryKeyClick,
diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/GlobalSearchPresenterTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/GlobalSearchPresenterTest.kt
new file mode 100644
index 0000000000..94c19bb81d
--- /dev/null
+++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/search/GlobalSearchPresenterTest.kt
@@ -0,0 +1,308 @@
+/*
+ * 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.features.home.impl.search
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.features.home.impl.datasource.aRoomListRoomSummaryFactory
+import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
+import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
+import io.element.android.libraries.architecture.AsyncData
+import io.element.android.libraries.dateformatter.api.DateFormatter
+import io.element.android.libraries.dateformatter.test.FakeDateFormatter
+import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
+import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventFormatter
+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.permalink.PermalinkParser
+import io.element.android.libraries.matrix.api.roomlist.RoomListService
+import io.element.android.libraries.matrix.api.search.MessageSearchService
+import io.element.android.libraries.matrix.test.FakeMatrixClient
+import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
+import io.element.android.libraries.matrix.test.room.aRoomSummary
+import io.element.android.libraries.matrix.test.roomlist.FakeDynamicRoomList
+import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
+import io.element.android.libraries.matrix.test.search.FakeMessageSearch
+import io.element.android.libraries.matrix.test.search.FakeMessageSearchService
+import io.element.android.tests.testutils.consumeItemsUntilPredicate
+import io.element.android.tests.testutils.test
+import io.element.android.tests.testutils.testCoroutineDispatchers
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import java.util.Optional
+
+class GlobalSearchPresenterTest {
+ @Test
+ fun `present - initial state`() = runTest {
+ val presenter = createGlobalSearchPresenter()
+ presenter.test {
+ awaitItem().let { state ->
+ assertThat(state.isEnabled).isFalse()
+ assertThat(state.isSearchActive).isFalse()
+ assertThat(state.queryState.text.toString()).isEmpty()
+ assertThat(state.currentTarget).isEqualTo(GlobalSearchTarget.ROOMS)
+ assertThat(state.results).isEqualTo(AsyncData.Uninitialized)
+ }
+ }
+ }
+
+ @Test
+ fun `present - isEnabled reflects the feature flag`() = runTest {
+ val presenter = createGlobalSearchPresenter(
+ featureFlagService = FakeFeatureFlagService(
+ initialState = mapOf(FeatureFlags.MessageSearch.key to true),
+ ),
+ )
+ presenter.test {
+ // isEnabled is emitted asynchronously via produceState, so it starts as false and flips to true
+ var state = awaitItem()
+ while (!state.isEnabled) {
+ state = awaitItem()
+ }
+ assertThat(state.isEnabled).isTrue()
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `present - toggle search visibility`() = runTest {
+ val presenter = createGlobalSearchPresenter()
+ presenter.test {
+ awaitItem().let { state ->
+ assertThat(state.isSearchActive).isFalse()
+ state.eventSink(GlobalSearchEvent.ToggleSearchVisibility)
+ }
+ awaitItem().let { state ->
+ assertThat(state.isSearchActive).isTrue()
+ state.eventSink(GlobalSearchEvent.ToggleSearchVisibility)
+ }
+ awaitItem().let { state ->
+ assertThat(state.isSearchActive).isFalse()
+ }
+ }
+ }
+
+ @Test
+ fun `present - update target`() = runTest {
+ val presenter = createGlobalSearchPresenter()
+ presenter.test {
+ awaitItem().let { state ->
+ assertThat(state.currentTarget).isEqualTo(GlobalSearchTarget.ROOMS)
+ state.eventSink(GlobalSearchEvent.UpdateTarget(GlobalSearchTarget.MESSAGES))
+ }
+ awaitItem().let { state ->
+ assertThat(state.currentTarget).isEqualTo(GlobalSearchTarget.MESSAGES)
+ }
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `present - clear query`() = runTest {
+ val presenter = createGlobalSearchPresenter()
+ presenter.test {
+ val initialState = awaitItem()
+ initialState.queryState.edit { append("A query") }
+ awaitItem().let { state ->
+ assertThat(state.queryState.text.toString()).isEqualTo("A query")
+ state.eventSink(GlobalSearchEvent.ClearQuery)
+ }
+ awaitItem().let { state ->
+ assertThat(state.queryState.text.toString()).isEmpty()
+ }
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `present - room search results are emitted when the room list changes`() = runTest {
+ val roomList = FakeDynamicRoomList()
+ val roomListService = FakeRoomListService(createRoomListLambda = { roomList })
+ val presenter = createGlobalSearchPresenter(roomListService = roomListService)
+ presenter.test {
+ val initialState = awaitItem()
+ assertThat(initialState.results).isEqualTo(AsyncData.Uninitialized)
+
+ initialState.queryState.edit { append("A query") }
+ // Let the query be propagated to the data source before emitting results
+ testScheduler.advanceUntilIdle()
+ roomList.summaries.emit(listOf(aRoomSummary()))
+
+ // Wait for the presenter to emit a loading state before the success state
+ consumeItemsUntilPredicate { it.results is AsyncData.Loading }
+
+ val successState = awaitItem()
+ val results = (successState.results.dataOrNull() as GlobalSearchResults.RoomListResults).results
+ assertThat(results).hasSize(1)
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `present - room search displays the uninitialized state when removing the query`() = runTest {
+ val roomList = FakeDynamicRoomList()
+ val roomListService = FakeRoomListService(createRoomListLambda = { roomList })
+ val presenter = createGlobalSearchPresenter(roomListService = roomListService)
+ presenter.test {
+ val initialState = awaitItem()
+ assertThat(initialState.results).isEqualTo(AsyncData.Uninitialized)
+
+ initialState.queryState.edit { append("A query") }
+ // Let the query be propagated to the data source before emitting results
+ testScheduler.advanceUntilIdle()
+ roomList.summaries.emit(listOf(aRoomSummary()))
+
+ // Wait for the presenter to emit a loading state before the success state
+ consumeItemsUntilPredicate { it.results is AsyncData.Loading }
+
+ val successState = awaitItem()
+ val results = (successState.results.dataOrNull() as GlobalSearchResults.RoomListResults).results
+ assertThat(results).hasSize(1)
+
+ // Remove the query
+ successState.queryState.edit { replace(0, length, "") }
+ // Skip the intermediate state where the query is empty but results are still present
+ skipItems(1)
+ // It's back to the uninitialized state
+ assertThat(awaitItem().results).isInstanceOf(AsyncData.Uninitialized::class.java)
+ // This happens even if for some reason the room results still contained any items
+ assertThat(roomList.summaries.value).isNotEmpty()
+ }
+ }
+
+ @Test
+ fun `present - message search results are mapped when the message search emits`() = runTest {
+ val messageSearch = FakeMessageSearch()
+ val matrixClient = FakeMatrixClient().apply {
+ getRoomInfoFlowLambda = { flowOf(Optional.of(aRoomInfo())) }
+ }
+ val presenter = createGlobalSearchPresenter(
+ messageSearchService = FakeMessageSearchService(messageSearch),
+ matrixClient = matrixClient,
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ initialState.eventSink(GlobalSearchEvent.UpdateTarget(GlobalSearchTarget.MESSAGES))
+ initialState.queryState.edit { append("A query") }
+ testScheduler.advanceUntilIdle()
+
+ messageSearch.emitResults(persistentListOf(aMessageSearchResult()))
+ testScheduler.advanceUntilIdle()
+
+ val successState = consumeItemsUntilPredicate { it.results is AsyncData.Success }.last()
+ val results = (successState.results.dataOrNull() as GlobalSearchResults.MessageSearchResults).results
+ assertThat(results).hasSize(1)
+ assertThat(results.first()).isInstanceOf(MessageSearchResultItem.Message::class.java)
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `present - message search displays the uninitialized state after removing the query`() = runTest {
+ val messageSearch = FakeMessageSearch()
+ val matrixClient = FakeMatrixClient().apply {
+ getRoomInfoFlowLambda = { flowOf(Optional.of(aRoomInfo())) }
+ }
+ val presenter = createGlobalSearchPresenter(
+ messageSearchService = FakeMessageSearchService(messageSearch),
+ matrixClient = matrixClient,
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ initialState.eventSink(GlobalSearchEvent.UpdateTarget(GlobalSearchTarget.MESSAGES))
+ initialState.queryState.edit { append("A query") }
+ testScheduler.advanceUntilIdle()
+
+ messageSearch.emitResults(persistentListOf(aMessageSearchResult()))
+ testScheduler.advanceUntilIdle()
+
+ val successState = consumeItemsUntilPredicate { it.results is AsyncData.Success }.last()
+ val results = (successState.results.dataOrNull() as GlobalSearchResults.MessageSearchResults).results
+ assertThat(results).hasSize(1)
+ assertThat(results.first()).isInstanceOf(MessageSearchResultItem.Message::class.java)
+
+ // Remove the query
+ successState.queryState.edit { replace(0, length, "") }
+ // Skip the intermediate state where the query is empty but results are still present
+ skipItems(1)
+ // It's back to the uninitialized state
+ assertThat(awaitItem().results).isInstanceOf(AsyncData.Uninitialized::class.java)
+ // This happens even if for some reason the message results still contained any items
+ assertThat(messageSearch.results.value).isNotEmpty()
+ }
+ }
+
+ @Test
+ fun `present - UpdateVisibleRange triggers pagination for messages when near the end`() = runTest {
+ val messageSearch = FakeMessageSearch()
+ val matrixClient = FakeMatrixClient().apply {
+ getRoomInfoFlowLambda = { flowOf(Optional.of(aRoomInfo())) }
+ }
+ val presenter = createGlobalSearchPresenter(
+ messageSearchService = FakeMessageSearchService(messageSearch),
+ matrixClient = matrixClient,
+ )
+ presenter.test {
+ val initialState = awaitItem()
+ initialState.eventSink(GlobalSearchEvent.UpdateTarget(GlobalSearchTarget.MESSAGES))
+ initialState.queryState.edit { append("A query") }
+ testScheduler.advanceUntilIdle()
+
+ messageSearch.emitResults(persistentListOf(aMessageSearchResult()))
+ val successState = consumeItemsUntilPredicate { it.results is AsyncData.Success }.last()
+
+ successState.eventSink(GlobalSearchEvent.UpdateVisibleRange(IntRange(0, 0)))
+ runCurrent()
+ testScheduler.advanceUntilIdle()
+
+ assertThat(messageSearch.paginateCallCount).isEqualTo(1)
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+}
+
+private fun TestScope.createGlobalSearchPresenter(
+ roomListService: RoomListService = FakeRoomListService(),
+ messageSearchService: MessageSearchService = FakeMessageSearchService(),
+ featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
+ latestEventFormatter: RoomLatestEventFormatter = FakeRoomLatestEventFormatter(),
+ dateFormatter: DateFormatter = FakeDateFormatter(),
+ fileSizeFormatter: FileSizeFormatter = FakeFileSizeFormatter(),
+ permalinkParser: PermalinkParser = FakePermalinkParser(),
+ matrixClient: MatrixClient = FakeMatrixClient(),
+): GlobalSearchPresenter {
+ return GlobalSearchPresenter(
+ roomListSearchDataSourceFactory = object : RoomListSearchDataSource.Factory {
+ override fun create(coroutineScope: CoroutineScope): RoomListSearchDataSource {
+ return RoomListSearchDataSource(
+ coroutineScope = coroutineScope,
+ roomListService = roomListService,
+ coroutineDispatchers = testCoroutineDispatchers(),
+ roomSummaryFactory = aRoomListRoomSummaryFactory(),
+ )
+ }
+ },
+ messageSearchService = messageSearchService,
+ featureFlagService = featureFlagService,
+ latestEventFormatter = latestEventFormatter,
+ dateFormatter = dateFormatter,
+ fileSizeFormatter = fileSizeFormatter,
+ permalinkParser = permalinkParser,
+ coroutineDispatchers = testCoroutineDispatchers(),
+ matrixClient = matrixClient,
+ )
+}
diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvents.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvent.kt
similarity index 91%
rename from features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvents.kt
rename to features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvent.kt
index 5691b19ef7..1bf1ffea8a 100644
--- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvents.kt
+++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteEvent.kt
@@ -13,7 +13,7 @@ import io.element.android.features.invite.api.InviteData
/**
* Events sent to the accept/decline invite presenter, from whichever screen is showing the invitation.
*/
-interface AcceptDeclineInviteEvents {
- data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents
- data class DeclineInvite(val invite: InviteData, val blockUser: Boolean, val shouldConfirm: Boolean) : AcceptDeclineInviteEvents
+interface AcceptDeclineInviteEvent {
+ data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvent
+ data class DeclineInvite(val invite: InviteData, val blockUser: Boolean, val shouldConfirm: Boolean) : AcceptDeclineInviteEvent
}
diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteState.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteState.kt
index 3bfd0430fe..3e9c549620 100644
--- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteState.kt
+++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteState.kt
@@ -14,5 +14,5 @@ import io.element.android.libraries.matrix.api.core.RoomId
data class AcceptDeclineInviteState(
val acceptAction: AsyncAction,
val declineAction: AsyncAction,
- val eventSink: (AcceptDeclineInviteEvents) -> Unit,
+ val eventSink: (AcceptDeclineInviteEvent) -> Unit,
)
diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStatePreviewParam.kt
similarity index 92%
rename from features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt
rename to features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStatePreviewParam.kt
index 18acc11e5e..ceda9542ef 100644
--- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStateProvider.kt
+++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/acceptdecline/AcceptDeclineInviteStatePreviewParam.kt
@@ -14,7 +14,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
fun anAcceptDeclineInviteState(
acceptAction: AsyncAction = AsyncAction.Uninitialized,
declineAction: AsyncAction = AsyncAction.Uninitialized,
- eventSink: (AcceptDeclineInviteEvents) -> Unit = {},
+ eventSink: (AcceptDeclineInviteEvent) -> Unit = {},
) = AcceptDeclineInviteState(
acceptAction = acceptAction,
declineAction = declineAction,
diff --git a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/declineandblock/DeclineInviteAndBlockEntryPoint.kt b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/declineandblock/DeclineInviteAndBlockEntryPoint.kt
index 4174d99222..3b9d512d80 100644
--- a/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/declineandblock/DeclineInviteAndBlockEntryPoint.kt
+++ b/features/invite/api/src/main/kotlin/io/element/android/features/invite/api/declineandblock/DeclineInviteAndBlockEntryPoint.kt
@@ -10,13 +10,19 @@ package io.element.android.features.invite.api.declineandblock
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.plugin.Plugin
import io.element.android.features.invite.api.InviteData
import io.element.android.libraries.architecture.FeatureEntryPoint
-fun interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint {
+interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint {
fun createNode(
parentNode: Node,
buildContext: BuildContext,
inviteData: InviteData,
+ callback: Callback,
): Node
+
+ interface Callback : Plugin {
+ fun onDeclineSuccess()
+ }
}
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenter.kt
index 9be255fe1a..e5ee3d20c7 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenter.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenter.kt
@@ -15,7 +15,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.features.invite.api.InviteData
-import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
+import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
import io.element.android.features.invite.impl.AcceptInvite
@@ -40,13 +40,13 @@ class AcceptDeclineInvitePresenter(
val declinedAction: MutableState> =
remember { mutableStateOf(AsyncAction.Uninitialized) }
- fun handleEvent(event: AcceptDeclineInviteEvents) {
+ fun handleEvent(event: AcceptDeclineInviteEvent) {
when (event) {
- is AcceptDeclineInviteEvents.AcceptInvite -> {
+ is AcceptDeclineInviteEvent.AcceptInvite -> {
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
}
- is AcceptDeclineInviteEvents.DeclineInvite -> {
+ is AcceptDeclineInviteEvent.DeclineInvite -> {
val inviteData = event.invite
if (event.shouldConfirm) {
declinedAction.value = ConfirmingDeclineInvite(inviteData, event.blockUser)
@@ -58,11 +58,11 @@ class AcceptDeclineInvitePresenter(
)
}
}
- is InternalAcceptDeclineInviteEvents.ClearAcceptActionState -> {
+ is InternalAcceptDeclineInviteEvent.ClearAcceptActionState -> {
acceptedAction.value = AsyncAction.Uninitialized
}
- is InternalAcceptDeclineInviteEvents.ClearDeclineActionState -> {
+ is InternalAcceptDeclineInviteEvent.ClearDeclineActionState -> {
declinedAction.value = AsyncAction.Uninitialized
}
}
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStatePreviewParam.kt
similarity index 95%
rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt
rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStatePreviewParam.kt
index db2e76c1c3..ad9bf01667 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStateProvider.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteStatePreviewParam.kt
@@ -18,7 +18,7 @@ import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.preview.ROOM_NAME
import io.element.android.libraries.matrix.api.core.RoomId
-open class AcceptDeclineInviteStateProvider : PreviewParameterProvider {
+open class AcceptDeclineInviteStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
anAcceptDeclineInviteState(),
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteView.kt
index da035f0ff1..21c59b1860 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteView.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInviteView.kt
@@ -14,7 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.invite.api.InviteData
-import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
+import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
import io.element.android.features.invite.impl.AcceptInvite
@@ -37,11 +37,11 @@ fun AcceptDeclineInviteView(
AsyncActionView(
async = state.acceptAction,
onSuccess = { roomId ->
- state.eventSink(InternalAcceptDeclineInviteEvents.ClearAcceptActionState)
+ state.eventSink(InternalAcceptDeclineInviteEvent.ClearAcceptActionState)
onAcceptInviteSuccess(roomId)
},
onErrorDismiss = {
- state.eventSink(InternalAcceptDeclineInviteEvents.ClearAcceptActionState)
+ state.eventSink(InternalAcceptDeclineInviteEvent.ClearAcceptActionState)
},
errorTitle = {
stringResource(CommonStrings.common_something_went_wrong)
@@ -57,11 +57,11 @@ fun AcceptDeclineInviteView(
AsyncActionView(
async = state.declineAction,
onSuccess = { roomId ->
- state.eventSink(InternalAcceptDeclineInviteEvents.ClearDeclineActionState)
+ state.eventSink(InternalAcceptDeclineInviteEvent.ClearDeclineActionState)
onDeclineInviteSuccess(roomId)
},
onErrorDismiss = {
- state.eventSink(InternalAcceptDeclineInviteEvents.ClearDeclineActionState)
+ state.eventSink(InternalAcceptDeclineInviteEvent.ClearDeclineActionState)
},
errorTitle = {
stringResource(CommonStrings.common_something_went_wrong)
@@ -77,7 +77,7 @@ fun AcceptDeclineInviteView(
blockUser = confirming.blockUser,
onConfirmClick = {
state.eventSink(
- AcceptDeclineInviteEvents.DeclineInvite(
+ AcceptDeclineInviteEvent.DeclineInvite(
confirming.inviteData,
blockUser = confirming.blockUser,
shouldConfirm = false
@@ -85,7 +85,7 @@ fun AcceptDeclineInviteView(
)
},
onDismissClick = {
- state.eventSink(InternalAcceptDeclineInviteEvents.ClearDeclineActionState)
+ state.eventSink(InternalAcceptDeclineInviteEvent.ClearDeclineActionState)
}
)
}
@@ -123,7 +123,7 @@ private fun DeclineConfirmationDialog(
@PreviewsDayNight
@Composable
-internal fun AcceptDeclineInviteViewPreview(@PreviewParameter(AcceptDeclineInviteStateProvider::class) state: AcceptDeclineInviteState) =
+internal fun AcceptDeclineInviteViewPreview(@PreviewParameter(AcceptDeclineInviteStatePreviewParam::class) state: AcceptDeclineInviteState) =
ElementPreview {
AcceptDeclineInviteView(
state = state,
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/InternalAcceptDeclineInviteEvents.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/InternalAcceptDeclineInviteEvent.kt
similarity index 79%
rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/InternalAcceptDeclineInviteEvents.kt
rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/InternalAcceptDeclineInviteEvent.kt
index 765c7055d3..805ed9871e 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/InternalAcceptDeclineInviteEvents.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/acceptdecline/InternalAcceptDeclineInviteEvent.kt
@@ -8,9 +8,9 @@
package io.element.android.features.invite.impl.acceptdecline
-import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
+import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
-sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents {
- data object ClearAcceptActionState : InternalAcceptDeclineInviteEvents
- data object ClearDeclineActionState : InternalAcceptDeclineInviteEvents
+sealed interface InternalAcceptDeclineInviteEvent : AcceptDeclineInviteEvent {
+ data object ClearAcceptActionState : InternalAcceptDeclineInviteEvent
+ data object ClearDeclineActionState : InternalAcceptDeclineInviteEvent
}
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockEvents.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockEvent.kt
similarity index 57%
rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockEvents.kt
rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockEvent.kt
index 1fc160e530..f31b9d042f 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockEvents.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockEvent.kt
@@ -8,10 +8,10 @@
package io.element.android.features.invite.impl.declineandblock
-sealed interface DeclineAndBlockEvents {
- data class UpdateReportReason(val reason: String) : DeclineAndBlockEvents
- data object ToggleReportRoom : DeclineAndBlockEvents
- data object ToggleBlockUser : DeclineAndBlockEvents
- data object Decline : DeclineAndBlockEvents
- data object ClearDeclineAction : DeclineAndBlockEvents
+sealed interface DeclineAndBlockEvent {
+ data class UpdateReportReason(val reason: String) : DeclineAndBlockEvent
+ data object ToggleReportRoom : DeclineAndBlockEvent
+ data object ToggleBlockUser : DeclineAndBlockEvent
+ data object Decline : DeclineAndBlockEvent
+ data object ClearDeclineAction : DeclineAndBlockEvent
}
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockNode.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockNode.kt
index 2fe5c23088..89a2aaf238 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockNode.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockNode.kt
@@ -17,7 +17,9 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.invite.api.InviteData
+import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
import io.element.android.libraries.architecture.NodeInputs
+import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
@@ -31,6 +33,7 @@ class DeclineAndBlockNode(
data class Inputs(val inviteData: InviteData) : NodeInputs
private val inviteData = inputs().inviteData
+ private val callback: DeclineInviteAndBlockEntryPoint.Callback = callback()
private val presenter = presenterFactory.create(inviteData)
@Composable
@@ -39,6 +42,7 @@ class DeclineAndBlockNode(
DeclineAndBlockView(
state = state,
onBackClick = ::navigateUp,
+ onDeclineSuccess = callback::onDeclineSuccess,
modifier = modifier
)
}
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenter.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenter.kt
index af2a10cbfd..170d003bea 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenter.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenter.kt
@@ -49,13 +49,13 @@ class DeclineAndBlockPresenter(
val coroutineScope = rememberCoroutineScope()
- fun handleEvent(event: DeclineAndBlockEvents) {
+ fun handleEvent(event: DeclineAndBlockEvent) {
when (event) {
- DeclineAndBlockEvents.ClearDeclineAction -> declineAction.value = AsyncAction.Uninitialized
- DeclineAndBlockEvents.Decline -> coroutineScope.decline(reportReason, blockUser, reportRoom, declineAction)
- DeclineAndBlockEvents.ToggleBlockUser -> blockUser = !blockUser
- DeclineAndBlockEvents.ToggleReportRoom -> reportRoom = !reportRoom
- is DeclineAndBlockEvents.UpdateReportReason -> reportReason = event.reason
+ DeclineAndBlockEvent.ClearDeclineAction -> declineAction.value = AsyncAction.Uninitialized
+ DeclineAndBlockEvent.Decline -> coroutineScope.decline(reportReason, blockUser, reportRoom, declineAction)
+ DeclineAndBlockEvent.ToggleBlockUser -> blockUser = !blockUser
+ DeclineAndBlockEvent.ToggleReportRoom -> reportRoom = !reportRoom
+ is DeclineAndBlockEvent.UpdateReportReason -> reportReason = event.reason
}
}
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockState.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockState.kt
index 7dbbbfd60e..057929ea42 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockState.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockState.kt
@@ -15,7 +15,7 @@ data class DeclineAndBlockState(
val reportReason: String,
val blockUser: Boolean,
val declineAction: AsyncAction,
- val eventSink: (DeclineAndBlockEvents) -> Unit
+ val eventSink: (DeclineAndBlockEvent) -> Unit
) {
val canDecline = blockUser || reportRoom && reportReason.isNotEmpty()
}
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockStateProvider.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockStatePreviewParam.kt
similarity index 90%
rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockStateProvider.kt
rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockStatePreviewParam.kt
index 2e78214306..6690a568dd 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockStateProvider.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockStatePreviewParam.kt
@@ -11,7 +11,7 @@ package io.element.android.features.invite.impl.declineandblock
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
-open class DeclineAndBlockStateProvider : PreviewParameterProvider {
+open class DeclineAndBlockStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aDeclineAndBlockState(),
@@ -36,7 +36,7 @@ fun aDeclineAndBlockState(
reportReason: String = "",
blockUser: Boolean = false,
declineAction: AsyncAction = AsyncAction.Uninitialized,
- eventSink: (DeclineAndBlockEvents) -> Unit = {},
+ eventSink: (DeclineAndBlockEvent) -> Unit = {},
) = DeclineAndBlockState(
reportRoom = reportRoom,
reportReason = reportReason,
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockView.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockView.kt
index 1d14df024a..eaace62309 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockView.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockView.kt
@@ -46,6 +46,7 @@ import io.element.android.libraries.ui.strings.CommonStrings
fun DeclineAndBlockView(
state: DeclineAndBlockState,
onBackClick: () -> Unit,
+ onDeclineSuccess: () -> Unit,
modifier: Modifier = Modifier,
) {
val focusManager = LocalFocusManager.current
@@ -53,10 +54,10 @@ fun DeclineAndBlockView(
val isDeclining = state.declineAction is AsyncAction.Loading
AsyncActionView(
async = state.declineAction,
- onSuccess = { onBackClick() },
+ onSuccess = { onDeclineSuccess() },
errorMessage = { stringResource(CommonStrings.error_unknown) },
- onRetry = { state.eventSink(DeclineAndBlockEvents.Decline) },
- onErrorDismiss = { state.eventSink(DeclineAndBlockEvents.ClearDeclineAction) }
+ onRetry = { state.eventSink(DeclineAndBlockEvent.Decline) },
+ onErrorDismiss = { state.eventSink(DeclineAndBlockEvent.ClearDeclineAction) }
)
Scaffold(
@@ -88,7 +89,7 @@ fun DeclineAndBlockView(
Text(text = stringResource(R.string.screen_decline_and_block_block_user_option_description))
},
onClick = {
- state.eventSink(DeclineAndBlockEvents.ToggleBlockUser)
+ state.eventSink(DeclineAndBlockEvent.ToggleBlockUser)
},
trailingContent = ListItemContent.Switch(checked = state.blockUser)
)
@@ -103,7 +104,7 @@ fun DeclineAndBlockView(
Text(text = stringResource(R.string.screen_decline_and_block_report_user_option_description))
},
onClick = {
- state.eventSink(DeclineAndBlockEvents.ToggleReportRoom)
+ state.eventSink(DeclineAndBlockEvent.ToggleReportRoom)
},
trailingContent = ListItemContent.Switch(checked = state.reportRoom)
)
@@ -112,7 +113,7 @@ fun DeclineAndBlockView(
Spacer(modifier = Modifier.height(24.dp))
TextField(
value = state.reportReason,
- onValueChange = { state.eventSink(DeclineAndBlockEvents.UpdateReportReason(it)) },
+ onValueChange = { state.eventSink(DeclineAndBlockEvent.UpdateReportReason(it)) },
placeholder = stringResource(R.string.screen_decline_and_block_report_user_reason_placeholder),
minLines = 3,
enabled = !isDeclining,
@@ -131,7 +132,7 @@ fun DeclineAndBlockView(
enabled = !isDeclining && state.canDecline,
onClick = {
focusManager.clearFocus(force = true)
- state.eventSink(DeclineAndBlockEvents.Decline)
+ state.eventSink(DeclineAndBlockEvent.Decline)
},
modifier = Modifier
.fillMaxWidth()
@@ -144,10 +145,11 @@ fun DeclineAndBlockView(
@PreviewsDayNight
@Composable
internal fun DeclineAndBlockViewPreview(
- @PreviewParameter(DeclineAndBlockStateProvider::class) state: DeclineAndBlockState
+ @PreviewParameter(DeclineAndBlockStatePreviewParam::class) state: DeclineAndBlockState
) = ElementPreview {
DeclineAndBlockView(
state = state,
onBackClick = {},
+ onDeclineSuccess = {},
)
}
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPoint.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPoint.kt
index d6e93fb805..4a5734af45 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPoint.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPoint.kt
@@ -22,8 +22,9 @@ class DefaultDeclineAndBlockEntryPoint : DeclineInviteAndBlockEntryPoint {
parentNode: Node,
buildContext: BuildContext,
inviteData: InviteData,
+ callback: DeclineInviteAndBlockEntryPoint.Callback,
): Node {
val inputs = DeclineAndBlockNode.Inputs(inviteData)
- return parentNode.createNode(buildContext, plugins = listOf(inputs))
+ return parentNode.createNode(buildContext, plugins = listOf(inputs, callback))
}
}
diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteBindingContainer.kt
similarity index 89%
rename from features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt
rename to features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteBindingContainer.kt
index f78c0358a6..f7cd346755 100644
--- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt
+++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteBindingContainer.kt
@@ -22,9 +22,9 @@ import io.element.android.libraries.matrix.api.MatrixClient
@ContributesTo(SessionScope::class)
@BindingContainer
-interface InviteModule {
+interface InviteBindingContainer {
@Binds
- fun bindAcceptDeclinePresenter(presenter: AcceptDeclineInvitePresenter): Presenter
+ fun bindAcceptDeclineInvitePresenter(presenter: AcceptDeclineInvitePresenter): Presenter
companion object {
@Provides
diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenterTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenterTest.kt
index ba080293f8..15624ba9be 100644
--- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenterTest.kt
+++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/acceptdecline/AcceptDeclineInvitePresenterTest.kt
@@ -10,7 +10,7 @@ package io.element.android.features.invite.impl.acceptdecline
import com.google.common.truth.Truth.assertThat
import io.element.android.features.invite.api.InviteData
-import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
+import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
import io.element.android.features.invite.impl.AcceptInvite
import io.element.android.features.invite.impl.DeclineInvite
@@ -51,13 +51,13 @@ class AcceptDeclineInvitePresenterTest {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
- AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
+ AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink(
- InternalAcceptDeclineInviteEvents.ClearDeclineActionState
+ InternalAcceptDeclineInviteEvent.ClearDeclineActionState
)
}
awaitItem().also { state ->
@@ -78,20 +78,20 @@ class AcceptDeclineInvitePresenterTest {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
- AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
+ AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink(
- AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false)
+ AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false)
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(
- InternalAcceptDeclineInviteEvents.ClearDeclineActionState
+ InternalAcceptDeclineInviteEvent.ClearDeclineActionState
)
}
awaitItem().also { state ->
@@ -114,13 +114,13 @@ class AcceptDeclineInvitePresenterTest {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
- AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
+ AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, blockUser = false))
state.eventSink(
- AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false)
+ AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false)
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
@@ -146,7 +146,7 @@ class AcceptDeclineInvitePresenterTest {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
- AcceptDeclineInviteEvents.AcceptInvite(inviteData)
+ AcceptDeclineInviteEvent.AcceptInvite(inviteData)
)
}
awaitItem().also { state ->
@@ -155,7 +155,7 @@ class AcceptDeclineInvitePresenterTest {
awaitItem().also { state ->
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(
- InternalAcceptDeclineInviteEvents.ClearAcceptActionState
+ InternalAcceptDeclineInviteEvent.ClearAcceptActionState
)
}
awaitItem().also { state ->
@@ -178,7 +178,7 @@ class AcceptDeclineInvitePresenterTest {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
- AcceptDeclineInviteEvents.AcceptInvite(inviteData)
+ AcceptDeclineInviteEvent.AcceptInvite(inviteData)
)
}
awaitItem().also { state ->
diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenterTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenterTest.kt
index 67c2d55490..61c67c34b3 100644
--- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenterTest.kt
+++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockPresenterTest.kt
@@ -53,21 +53,21 @@ class DeclineAndBlockPresenterTest {
assertThat(state.blockUser).isTrue()
assertThat(state.reportReason).isEmpty()
assertThat(state.canDecline).isTrue()
- state.eventSink(DeclineAndBlockEvents.ToggleBlockUser)
+ state.eventSink(DeclineAndBlockEvent.ToggleBlockUser)
}
awaitItem().also { state ->
assertThat(state.reportRoom).isFalse()
assertThat(state.blockUser).isFalse()
assertThat(state.reportReason).isEmpty()
assertThat(state.canDecline).isFalse()
- state.eventSink(DeclineAndBlockEvents.ToggleReportRoom)
+ state.eventSink(DeclineAndBlockEvent.ToggleReportRoom)
}
awaitItem().also { state ->
assertThat(state.reportRoom).isTrue()
assertThat(state.blockUser).isFalse()
assertThat(state.reportReason).isEmpty()
assertThat(state.canDecline).isFalse()
- state.eventSink(DeclineAndBlockEvents.UpdateReportReason("Spam"))
+ state.eventSink(DeclineAndBlockEvent.UpdateReportReason("Spam"))
}
awaitItem().also { state ->
assertThat(state.reportRoom).isTrue()
@@ -87,7 +87,7 @@ class DeclineAndBlockPresenterTest {
)
presenter.test {
awaitItem().also { state ->
- state.eventSink(DeclineAndBlockEvents.Decline)
+ state.eventSink(DeclineAndBlockEvent.Decline)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
@@ -110,12 +110,12 @@ class DeclineAndBlockPresenterTest {
)
presenter.test {
awaitItem().also { state ->
- state.eventSink(DeclineAndBlockEvents.Decline)
+ state.eventSink(DeclineAndBlockEvent.Decline)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
- state.eventSink(DeclineAndBlockEvents.ClearDeclineAction)
+ state.eventSink(DeclineAndBlockEvent.ClearDeclineAction)
}
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
@@ -137,7 +137,7 @@ class DeclineAndBlockPresenterTest {
)
presenter.test {
awaitItem().also { state ->
- state.eventSink(DeclineAndBlockEvents.Decline)
+ state.eventSink(DeclineAndBlockEvent.Decline)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt
index 55c31a09d2..d3505ae5b2 100644
--- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt
+++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DeclineAndBlockViewTest.kt
@@ -17,6 +17,7 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import io.element.android.features.invite.impl.R
+import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
@@ -29,7 +30,7 @@ import org.junit.Test
class DeclineAndBlockViewTest : RobolectricTest() {
@Test
fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder(expectEvents = false)
+ val eventsRecorder = EventsRecorder(expectEvents = false)
ensureCalledOnce {
setDeclineAndBlockView(
aDeclineAndBlockState(
@@ -41,9 +42,23 @@ class DeclineAndBlockViewTest : RobolectricTest() {
}
}
+ @Test
+ fun `a successful decline invokes the expected callback`() = runAndroidComposeUiTest {
+ val eventsRecorder = EventsRecorder(expectEvents = false)
+ ensureCalledOnce {
+ setDeclineAndBlockView(
+ aDeclineAndBlockState(
+ declineAction = AsyncAction.Success(Unit),
+ eventSink = eventsRecorder,
+ ),
+ onDeclineSuccess = it,
+ )
+ }
+ }
+
@Test
fun `clicking on decline when enabled emits the expected event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setDeclineAndBlockView(
aDeclineAndBlockState(
blockUser = true,
@@ -51,12 +66,12 @@ class DeclineAndBlockViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_decline)
- eventsRecorder.assertSingle(DeclineAndBlockEvents.Decline)
+ eventsRecorder.assertSingle(DeclineAndBlockEvent.Decline)
}
@Test
fun `clicking on decline when disabled does not emit event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder(expectEvents = false)
+ val eventsRecorder = EventsRecorder(expectEvents = false)
setDeclineAndBlockView(
aDeclineAndBlockState(
blockUser = false,
@@ -70,7 +85,7 @@ class DeclineAndBlockViewTest : RobolectricTest() {
@Test
fun `clicking on block option emits the expected event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setDeclineAndBlockView(
aDeclineAndBlockState(
blockUser = true,
@@ -78,12 +93,12 @@ class DeclineAndBlockViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_decline_and_block_block_user_option_title)
- eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleBlockUser)
+ eventsRecorder.assertSingle(DeclineAndBlockEvent.ToggleBlockUser)
}
@Test
fun `clicking on report room option emits the expected event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setDeclineAndBlockView(
aDeclineAndBlockState(
reportRoom = true,
@@ -91,12 +106,12 @@ class DeclineAndBlockViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_report_room)
- eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleReportRoom)
+ eventsRecorder.assertSingle(DeclineAndBlockEvent.ToggleReportRoom)
}
@Test
fun `typing text in the reason field emits the expected Event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setDeclineAndBlockView(
aDeclineAndBlockState(
reportRoom = true,
@@ -105,18 +120,20 @@ class DeclineAndBlockViewTest : RobolectricTest() {
),
)
onNodeWithText("").performTextInput("Spam!")
- eventsRecorder.assertSingle(DeclineAndBlockEvents.UpdateReportReason("Spam!"))
+ eventsRecorder.assertSingle(DeclineAndBlockEvent.UpdateReportReason("Spam!"))
}
}
private fun AndroidComposeUiTest.setDeclineAndBlockView(
state: DeclineAndBlockState,
onBackClick: () -> Unit = EnsureNeverCalled(),
+ onDeclineSuccess: () -> Unit = EnsureNeverCalled(),
) {
setContent {
DeclineAndBlockView(
state = state,
onBackClick = onBackClick,
+ onDeclineSuccess = onDeclineSuccess,
)
}
}
diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPointTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPointTest.kt
index 13bb13dccf..f09cda2337 100644
--- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPointTest.kt
+++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/declineandblock/DefaultDeclineAndBlockEntryPointTest.kt
@@ -11,7 +11,9 @@ package io.element.android.features.invite.impl.declineandblock
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext
import com.google.common.truth.Truth.assertThat
+import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
import io.element.android.features.invite.test.anInviteData
+import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.node.TestParentNode
import org.junit.Rule
import org.junit.Test
@@ -31,12 +33,17 @@ class DefaultDeclineAndBlockEntryPointTest {
)
}
val inviteData = anInviteData()
+ val callback = object : DeclineInviteAndBlockEntryPoint.Callback {
+ override fun onDeclineSuccess() = lambdaError()
+ }
val result = entryPoint.createNode(
parentNode = parentNode,
buildContext = BuildContext.root(null),
- inviteData = inviteData
+ inviteData = inviteData,
+ callback = callback,
)
assertThat(result).isInstanceOf(DeclineAndBlockNode::class.java)
assertThat(result.plugins).contains(DeclineAndBlockNode.Inputs(inviteData))
+ assertThat(result.plugins).contains(callback)
}
}
diff --git a/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/declineandblock/FakeDeclineInviteAndBlockEntryPoint.kt b/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/declineandblock/FakeDeclineInviteAndBlockEntryPoint.kt
index 52d4d17790..19625f1473 100644
--- a/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/declineandblock/FakeDeclineInviteAndBlockEntryPoint.kt
+++ b/features/invite/test/src/main/kotlin/io/element/android/features/invite/test/declineandblock/FakeDeclineInviteAndBlockEntryPoint.kt
@@ -19,6 +19,7 @@ class FakeDeclineInviteAndBlockEntryPoint : DeclineInviteAndBlockEntryPoint {
parentNode: Node,
buildContext: BuildContext,
inviteData: InviteData,
+ callback: DeclineInviteAndBlockEntryPoint.Callback,
): Node {
lambdaError()
}
diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvent.kt
similarity index 68%
rename from features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt
rename to features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvent.kt
index 9570ab8152..884312da67 100644
--- a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvents.kt
+++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleEvent.kt
@@ -11,8 +11,8 @@ package io.element.android.features.invitepeople.api
/**
* Events the invite people UI sends to its presenter through [InvitePeopleState.eventSink].
*/
-interface InvitePeopleEvents {
- data object SendInvites : InvitePeopleEvents
- data object CloseSearch : InvitePeopleEvents
- data object ClearError : InvitePeopleEvents
+interface InvitePeopleEvent {
+ data object SendInvites : InvitePeopleEvent
+ data object CloseSearch : InvitePeopleEvent
+ data object ClearError : InvitePeopleEvent
}
diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt
index 0863ec7212..b35ed22f7e 100644
--- a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt
+++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt
@@ -28,5 +28,5 @@ interface InvitePeopleState {
val createRoomFromDmAction: AsyncAction
/** Where the UI sends its events. */
- val eventSink: (InvitePeopleEvents) -> Unit
+ val eventSink: (InvitePeopleEvent) -> Unit
}
diff --git a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStatePreviewParam.kt
similarity index 89%
rename from features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt
rename to features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStatePreviewParam.kt
index b233ed07ef..379d80440e 100644
--- a/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt
+++ b/features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStatePreviewParam.kt
@@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
-class InvitePeopleStateProvider : PreviewParameterProvider {
+class InvitePeopleStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aPreviewInvitePeopleState(),
@@ -27,7 +27,7 @@ private data class PreviewInvitePeopleState(
override val isSearchActive: Boolean,
override val sendInvitesAction: AsyncAction,
override val createRoomFromDmAction: AsyncAction,
- override val eventSink: (InvitePeopleEvents) -> Unit,
+ override val eventSink: (InvitePeopleEvent) -> Unit,
) : InvitePeopleState
private fun aPreviewInvitePeopleState(
@@ -35,7 +35,7 @@ private fun aPreviewInvitePeopleState(
isSearchActive: Boolean = false,
sendInvitesAction: AsyncAction = AsyncAction.Uninitialized,
createRoomFromDmAction: AsyncAction = AsyncAction.Uninitialized,
- eventSink: (InvitePeopleEvents) -> Unit = {},
+ eventSink: (InvitePeopleEvent) -> Unit = {},
) = PreviewInvitePeopleState(
canInvite = canInvite,
isSearchActive = isSearchActive,
diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvent.kt
similarity index 77%
rename from features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt
rename to features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvent.kt
index 449d0ce6ac..90d6fef59d 100644
--- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvents.kt
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleEvent.kt
@@ -8,12 +8,12 @@
package io.element.android.features.invitepeople.impl
-import io.element.android.features.invitepeople.api.InvitePeopleEvents
+import io.element.android.features.invitepeople.api.InvitePeopleEvent
import io.element.android.libraries.matrix.api.user.MatrixUser
-sealed interface DefaultInvitePeopleEvents : InvitePeopleEvents {
- data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvents
- data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvents
- data object DismissUnknownUsersModal : DefaultInvitePeopleEvents
- data object RemoveUnknownUsers : DefaultInvitePeopleEvents
+sealed interface DefaultInvitePeopleEvent : InvitePeopleEvent {
+ data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvent
+ data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvent
+ data object DismissUnknownUsersModal : DefaultInvitePeopleEvent
+ data object RemoveUnknownUsers : DefaultInvitePeopleEvent
}
diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt
index 79cd0c83be..dbf5c46291 100644
--- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt
@@ -24,7 +24,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesBinding
-import io.element.android.features.invitepeople.api.InvitePeopleEvents
+import io.element.android.features.invitepeople.api.InvitePeopleEvent
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
import io.element.android.features.invitepeople.api.InvitePeopleState
import io.element.android.libraries.architecture.AsyncAction
@@ -179,26 +179,26 @@ class DefaultInvitePeoplePresenter(
)
}
- fun handleEvent(event: InvitePeopleEvents) {
+ fun handleEvent(event: InvitePeopleEvent) {
when (event) {
// Dedicated `when` for exhaustivity.
- is DefaultInvitePeopleEvents -> when (event) {
- is DefaultInvitePeopleEvents.OnSearchActiveChanged -> {
+ is DefaultInvitePeopleEvent -> when (event) {
+ is DefaultInvitePeopleEvent.OnSearchActiveChanged -> {
searchActive = event.active
if (!event.active) {
queryState.clearText()
}
}
- is DefaultInvitePeopleEvents.ToggleUser -> {
+ is DefaultInvitePeopleEvent.ToggleUser -> {
selectedUsers.toggleUser(event.user)
searchResults.toggleUser(event.user)
// suggestions will automatically update via derivedStateOf when selectedUsers changes
}
- is DefaultInvitePeopleEvents.DismissUnknownUsersModal -> {
+ is DefaultInvitePeopleEvent.DismissUnknownUsersModal -> {
sendInvitesAction.value = AsyncAction.Uninitialized
}
- is DefaultInvitePeopleEvents.RemoveUnknownUsers -> {
+ is DefaultInvitePeopleEvent.RemoveUnknownUsers -> {
val usersToRemove = selectedUsers.value.filter { it in unknownUsers }
usersToRemove.forEach { user ->
selectedUsers.toggleUser(user)
@@ -207,7 +207,7 @@ class DefaultInvitePeoplePresenter(
sendInvitesAction.value = AsyncAction.Uninitialized
}
}
- is InvitePeopleEvents.SendInvites -> {
+ is InvitePeopleEvent.SendInvites -> {
if (unknownUsers.isNotEmpty() && sendInvitesAction.value !is ConfirmingUnknownUserInvitation) {
sendInvitesAction.value = ConfirmingUnknownUserInvitation(
unknownUsers
@@ -224,11 +224,11 @@ class DefaultInvitePeoplePresenter(
}
}
}
- is InvitePeopleEvents.CloseSearch -> {
+ is InvitePeopleEvent.CloseSearch -> {
searchActive = false
queryState.clearText()
}
- is InvitePeopleEvents.ClearError -> {
+ is InvitePeopleEvent.ClearError -> {
sendInvitesAction.value = AsyncAction.Uninitialized
createRoomFromDmAction.value = AsyncAction.Uninitialized
}
diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt
index 46e5d9f1a5..f1058d1a1b 100644
--- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt
@@ -9,7 +9,7 @@
package io.element.android.features.invitepeople.impl
import androidx.compose.foundation.text.input.TextFieldState
-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.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
@@ -29,5 +29,5 @@ data class DefaultInvitePeopleState(
override val sendInvitesAction: AsyncAction,
override val createRoomFromDmAction: AsyncAction,
val suggestions: ImmutableList,
- override val eventSink: (InvitePeopleEvents) -> Unit
+ override val eventSink: (InvitePeopleEvent) -> Unit
) : InvitePeopleState
diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStatePreviewParam.kt
similarity index 98%
rename from features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt
rename to features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStatePreviewParam.kt
index 1e41114406..9aad913856 100644
--- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStatePreviewParam.kt
@@ -26,7 +26,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
-internal class DefaultInvitePeopleStateProvider : PreviewParameterProvider {
+internal class DefaultInvitePeopleStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aDefaultInvitePeopleState(),
diff --git a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt
index 0ee3559aa3..4da2ec7cea 100644
--- a/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt
+++ b/features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/InvitePeopleView.kt
@@ -30,7 +30,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
-import io.element.android.features.invitepeople.api.InvitePeopleEvents
+import io.element.android.features.invitepeople.api.InvitePeopleEvent
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
@@ -102,7 +102,7 @@ private fun InvitePeopleContentView(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
fun toggleUser(user: MatrixUser) {
- state.eventSink(DefaultInvitePeopleEvents.ToggleUser(user))
+ state.eventSink(DefaultInvitePeopleEvent.ToggleUser(user))
}
InvitePeopleSearchBar(
@@ -114,7 +114,7 @@ private fun InvitePeopleContentView(
active = state.isSearchActive,
onActiveChange = {
state.eventSink(
- DefaultInvitePeopleEvents.OnSearchActiveChanged(
+ DefaultInvitePeopleEvent.OnSearchActiveChanged(
it
)
)
@@ -144,7 +144,7 @@ private fun InvitePeopleContentView(
CheckableUserRow(
checked = invitableUser.isSelected,
onCheckedChange = {
- state.eventSink(DefaultInvitePeopleEvents.ToggleUser(invitableUser.matrixUser))
+ state.eventSink(DefaultInvitePeopleEvent.ToggleUser(invitableUser.matrixUser))
},
data = CheckableUserRowData.Resolved(
avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.UserListItem),
@@ -163,9 +163,9 @@ private fun InvitePeopleContentView(
if (state.sendInvitesAction is ConfirmingUnknownUserInvitation) {
InvitePeopleConfirmModal(
users = state.sendInvitesAction.users,
- onDismiss = { state.eventSink.invoke(DefaultInvitePeopleEvents.DismissUnknownUsersModal) },
- onInvite = { state.eventSink.invoke(InvitePeopleEvents.SendInvites) },
- onRemove = { state.eventSink.invoke(DefaultInvitePeopleEvents.RemoveUnknownUsers) }
+ onDismiss = { state.eventSink.invoke(DefaultInvitePeopleEvent.DismissUnknownUsersModal) },
+ onInvite = { state.eventSink.invoke(InvitePeopleEvent.SendInvites) },
+ onRemove = { state.eventSink.invoke(DefaultInvitePeopleEvent.RemoveUnknownUsers) }
)
}
}
@@ -316,7 +316,7 @@ private fun InvitePeopleConfirmModal(
@PreviewsDayNight
@Composable
-internal fun InvitePeopleViewPreview(@PreviewParameter(DefaultInvitePeopleStateProvider::class) state: DefaultInvitePeopleState) =
+internal fun InvitePeopleViewPreview(@PreviewParameter(DefaultInvitePeopleStatePreviewParam::class) state: DefaultInvitePeopleState) =
ElementPreview {
InvitePeopleView(state = state)
}
diff --git a/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt
index 922fa73060..39cfc3afba 100644
--- a/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt
+++ b/features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt
@@ -11,7 +11,7 @@ package io.element.android.features.invitepeople.impl
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import app.cash.turbine.ReceiveTurbine
import com.google.common.truth.Truth.assertThat
-import io.element.android.features.invitepeople.api.InvitePeopleEvents
+import io.element.android.features.invitepeople.api.InvitePeopleEvent
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
@@ -88,13 +88,13 @@ internal class DefaultInvitePeoplePresenterTest {
val initialState = awaitItem()
skipItems(1)
- initialState.eventSink(DefaultInvitePeopleEvents.OnSearchActiveChanged(true))
+ initialState.eventSink(DefaultInvitePeopleEvent.OnSearchActiveChanged(true))
val resultState = awaitItemAsDefault()
assertThat(resultState.isSearchActive).isTrue()
resultState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
assertThat(awaitItemAsDefault().searchQuery.text.toString()).isEqualTo("some query")
- resultState.eventSink(InvitePeopleEvents.CloseSearch)
+ resultState.eventSink(InvitePeopleEvent.CloseSearch)
skipItems(1)
awaitItemAsDefault().also {
assertThat(it.isSearchActive).isFalse()
@@ -290,18 +290,18 @@ internal class DefaultInvitePeoplePresenterTest {
skipItems(1)
// When we toggle a user not in the list, they are added
- initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(aMatrixUser()))
+ initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(aMatrixUser()))
assertThat(awaitItemAsDefault().selectedUsers).containsExactly(aMatrixUser())
// Toggling a different user also adds them
- initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(aMatrixUser(id = A_USER_ID_2.value)))
+ initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(aMatrixUser(id = A_USER_ID_2.value)))
assertThat(awaitItemAsDefault().selectedUsers).containsExactly(
aMatrixUser(),
aMatrixUser(id = A_USER_ID_2.value)
)
// Toggling the first user removes them
- initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(aMatrixUser()))
+ initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(aMatrixUser()))
assertThat(awaitItemAsDefault().selectedUsers).containsExactly(aMatrixUser(id = A_USER_ID_2.value))
}
}
@@ -319,7 +319,7 @@ internal class DefaultInvitePeoplePresenterTest {
val selectedUser = aMatrixUser(displayName = "John Doe")
- initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
+ initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(selectedUser))
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
skipItems(1)
@@ -373,7 +373,7 @@ internal class DefaultInvitePeoplePresenterTest {
}
// And then a user is toggled
- initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
+ initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(selectedUser))
skipItems(1)
val resultState = awaitItemAsDefault()
@@ -414,13 +414,13 @@ internal class DefaultInvitePeoplePresenterTest {
repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
skipItems(1)
// And then a user is toggled
- initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
+ initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(selectedUser))
skipItems(1)
val resultState = awaitItemAsDefault()
// The results are updated...
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
// Send invites
- initialState.eventSink(InvitePeopleEvents.SendInvites)
+ initialState.eventSink(InvitePeopleEvent.SendInvites)
// Can't invite in the loading state
awaitItem().run {
@@ -468,13 +468,13 @@ internal class DefaultInvitePeoplePresenterTest {
repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
skipItems(1)
// And then a user is toggled
- initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
+ initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(selectedUser))
skipItems(1)
val resultState = awaitItemAsDefault()
// The results are updated...
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
// Send invites
- initialState.eventSink(InvitePeopleEvents.SendInvites)
+ initialState.eventSink(InvitePeopleEvent.SendInvites)
// Can't invite in the loading state
awaitItem().run {
@@ -645,17 +645,17 @@ internal class DefaultInvitePeoplePresenterTest {
skipItems(1)
// When we toggle a user not in the list, they are added, and we fetch their identity.
- initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
+ initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(alice))
delay(100)
- awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob))
+ awaitItemAsDefault().eventSink(DefaultInvitePeopleEvent.ToggleUser(bob))
delay(100)
- awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie))
+ awaitItemAsDefault().eventSink(DefaultInvitePeopleEvent.ToggleUser(charlie))
delay(100)
// If we do not have their identity cached, or fail to fetch it, we should mark them as unknown.
awaitItemAsDefault().run {
assertThat(selectedUsers).containsExactly(alice, bob, charlie)
- eventSink(InvitePeopleEvents.SendInvites)
+ eventSink(InvitePeopleEvent.SendInvites)
}
getUserIdentityResult.assertions().isCalledExactly(3).withSequence(
@@ -668,7 +668,7 @@ internal class DefaultInvitePeoplePresenterTest {
awaitItemAsDefault().run {
assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java)
assertThat(canInvite).isTrue()
- eventSink(InvitePeopleEvents.SendInvites)
+ eventSink(InvitePeopleEvent.SendInvites)
}
delay(1_000)
@@ -712,11 +712,11 @@ internal class DefaultInvitePeoplePresenterTest {
skipItems(1)
// When we toggle a user not in the list, they are added, and we fetch their identity.
- initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
+ initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(alice))
delay(100)
- awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob))
+ awaitItemAsDefault().eventSink(DefaultInvitePeopleEvent.ToggleUser(bob))
delay(100)
- awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie))
+ awaitItemAsDefault().eventSink(DefaultInvitePeopleEvent.ToggleUser(charlie))
delay(100)
// And the search is matching Alice and Bob
@@ -738,7 +738,7 @@ internal class DefaultInvitePeoplePresenterTest {
searchResults.users().map { Pair(it.matrixUser, it.isSelected) }
).containsExactly(Pair(alice, true), Pair(bob, true))
- eventSink(InvitePeopleEvents.SendInvites)
+ eventSink(InvitePeopleEvent.SendInvites)
}
getUserIdentityResult.assertions().isCalledExactly(3).withSequence(
@@ -751,7 +751,7 @@ internal class DefaultInvitePeoplePresenterTest {
awaitItemAsDefault().run {
assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java)
assertThat(canInvite).isTrue()
- eventSink(DefaultInvitePeopleEvents.RemoveUnknownUsers)
+ eventSink(DefaultInvitePeopleEvent.RemoveUnknownUsers)
}
// Selecting "remove" should remove all unknown users, but keeps those who are known.
@@ -794,16 +794,16 @@ internal class DefaultInvitePeoplePresenterTest {
skipItems(1)
// When we toggle a user not in the list, they are added, and we fetch their identity.
- initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
+ initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(alice))
delay(100)
- awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob))
+ awaitItemAsDefault().eventSink(DefaultInvitePeopleEvent.ToggleUser(bob))
delay(100)
- awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie))
+ awaitItemAsDefault().eventSink(DefaultInvitePeopleEvent.ToggleUser(charlie))
delay(100)
awaitItemAsDefault().run {
assertThat(selectedUsers).containsExactly(alice, bob, charlie)
- eventSink(InvitePeopleEvents.SendInvites)
+ eventSink(InvitePeopleEvent.SendInvites)
}
getUserIdentityResult.assertions().isCalledExactly(3).withSequence(
@@ -816,7 +816,7 @@ internal class DefaultInvitePeoplePresenterTest {
awaitItemAsDefault().run {
assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java)
assertThat(canInvite).isTrue()
- eventSink(DefaultInvitePeopleEvents.DismissUnknownUsersModal)
+ eventSink(DefaultInvitePeopleEvent.DismissUnknownUsersModal)
}
// Dismissing should not modify the selection at all
@@ -853,10 +853,10 @@ internal class DefaultInvitePeoplePresenterTest {
skipItems(1)
// We want to add a new user to a DM
- initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
+ initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(alice))
// And we send the invites
- initialState.eventSink(InvitePeopleEvents.SendInvites)
+ initialState.eventSink(InvitePeopleEvent.SendInvites)
skipItems(1)
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvent.kt
similarity index 60%
rename from features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt
rename to features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvent.kt
index 51245dd8f6..a2e53485ce 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvents.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomEvent.kt
@@ -10,15 +10,15 @@ package io.element.android.features.joinroom.impl
import io.element.android.features.invite.api.InviteData
-sealed interface JoinRoomEvents {
- data object RetryFetchingContent : JoinRoomEvents
- data object DismissErrorAndHideContent : JoinRoomEvents
- data object JoinRoom : JoinRoomEvents
- data object KnockRoom : JoinRoomEvents
- data object ForgetRoom : JoinRoomEvents
- data class CancelKnock(val requiresConfirmation: Boolean) : JoinRoomEvents
- data class UpdateKnockMessage(val message: String) : JoinRoomEvents
- data object ClearActionStates : JoinRoomEvents
- data class AcceptInvite(val inviteData: InviteData) : JoinRoomEvents
- data class DeclineInvite(val inviteData: InviteData, val blockUser: Boolean) : JoinRoomEvents
+sealed interface JoinRoomEvent {
+ data object RetryFetchingContent : JoinRoomEvent
+ data object DismissErrorAndHideContent : JoinRoomEvent
+ data object JoinRoom : JoinRoomEvent
+ data object KnockRoom : JoinRoomEvent
+ data object ForgetRoom : JoinRoomEvent
+ data class CancelKnock(val requiresConfirmation: Boolean) : JoinRoomEvent
+ data class UpdateKnockMessage(val message: String) : JoinRoomEvent
+ data object ClearActionStates : JoinRoomEvent
+ data class AcceptInvite(val inviteData: InviteData) : JoinRoomEvent
+ data class DeclineInvite(val inviteData: InviteData, val blockUser: Boolean) : JoinRoomEvent
}
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomFlowNode.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomFlowNode.kt
index 93f8f53412..493c180731 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomFlowNode.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomFlowNode.kt
@@ -69,6 +69,9 @@ class JoinRoomFlowNode(
parentNode = this,
buildContext = buildContext,
inviteData = navTarget.inviteData,
+ callback = object : DeclineInviteAndBlockEntryPoint.Callback {
+ override fun onDeclineSuccess() = navigateUp()
+ },
)
NavTarget.Root -> rootNode(buildContext)
}
@@ -99,7 +102,7 @@ class JoinRoomFlowNode(
acceptDeclineInviteView.Render(
state = state.acceptDeclineInviteState,
onAcceptInviteSuccess = {},
- onDeclineInviteSuccess = {},
+ onDeclineInviteSuccess = { navigateUp() },
modifier = Modifier
)
}
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt
index d257952209..85ae9cf392 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenter.kt
@@ -26,7 +26,7 @@ import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.SeenInvitesStore
-import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
+import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.toInviteData
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
@@ -151,37 +151,37 @@ class JoinRoomPresenter(
contentState.markRoomInviteAsSeen()
}
- fun handleEvent(event: JoinRoomEvents) {
+ fun handleEvent(event: JoinRoomEvent) {
when (event) {
- JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction)
- is JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction, knockMessage)
- is JoinRoomEvents.AcceptInvite -> {
+ JoinRoomEvent.JoinRoom -> coroutineScope.joinRoom(joinAction)
+ is JoinRoomEvent.KnockRoom -> coroutineScope.knockRoom(knockAction, knockMessage)
+ is JoinRoomEvent.AcceptInvite -> {
acceptDeclineInviteState.eventSink(
- AcceptDeclineInviteEvents.AcceptInvite(event.inviteData)
+ AcceptDeclineInviteEvent.AcceptInvite(event.inviteData)
)
}
- is JoinRoomEvents.DeclineInvite -> {
+ is JoinRoomEvent.DeclineInvite -> {
acceptDeclineInviteState.eventSink(
- AcceptDeclineInviteEvents.DeclineInvite(invite = event.inviteData, blockUser = event.blockUser, shouldConfirm = true)
+ AcceptDeclineInviteEvent.DeclineInvite(invite = event.inviteData, blockUser = event.blockUser, shouldConfirm = true)
)
}
- is JoinRoomEvents.CancelKnock -> coroutineScope.cancelKnockRoom(event.requiresConfirmation, cancelKnockAction)
- JoinRoomEvents.RetryFetchingContent -> {
+ is JoinRoomEvent.CancelKnock -> coroutineScope.cancelKnockRoom(event.requiresConfirmation, cancelKnockAction)
+ JoinRoomEvent.RetryFetchingContent -> {
retryCount++
}
- JoinRoomEvents.ClearActionStates -> {
+ JoinRoomEvent.ClearActionStates -> {
knockAction.value = AsyncAction.Uninitialized
joinAction.value = AsyncAction.Uninitialized
cancelKnockAction.value = AsyncAction.Uninitialized
forgetRoomAction.value = AsyncAction.Uninitialized
}
- is JoinRoomEvents.UpdateKnockMessage -> {
+ is JoinRoomEvent.UpdateKnockMessage -> {
knockMessage = event.message.take(MAX_KNOCK_MESSAGE_LENGTH)
}
- JoinRoomEvents.DismissErrorAndHideContent -> {
+ JoinRoomEvent.DismissErrorAndHideContent -> {
isDismissingContent = true
}
- JoinRoomEvents.ForgetRoom -> coroutineScope.forgetRoom(forgetRoomAction)
+ JoinRoomEvent.ForgetRoom -> coroutineScope.forgetRoom(forgetRoomAction)
}
}
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt
index e109bb1315..25f7765a7c 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomState.kt
@@ -37,7 +37,7 @@ data class JoinRoomState(
val knockMessage: String,
val hideInviteAvatars: Boolean,
val canReportRoom: Boolean,
- val eventSink: (JoinRoomEvents) -> Unit
+ val eventSink: (JoinRoomEvent) -> Unit
) {
val isJoinActionUnauthorized = joinAction is AsyncAction.Failure && joinAction.error is JoinRoom.Failures.UnauthorizedJoin
val joinAuthorisationStatus = when (contentState) {
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStatePreviewParam.kt
similarity index 98%
rename from features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt
rename to features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStatePreviewParam.kt
index 4bfa741321..a09e33aded 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStateProvider.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomStatePreviewParam.kt
@@ -30,7 +30,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.ui.model.InviteSender
import kotlinx.collections.immutable.toImmutableList
-open class JoinRoomStateProvider : PreviewParameterProvider {
+open class JoinRoomStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aJoinRoomState(
@@ -207,7 +207,7 @@ fun aJoinRoomState(
knockMessage: String = "",
hideInviteAvatars: Boolean = false,
canReportRoom: Boolean = true,
- eventSink: (JoinRoomEvents) -> Unit = {}
+ eventSink: (JoinRoomEvent) -> Unit = {}
) = JoinRoomState(
roomIdOrAlias = roomIdOrAlias,
contentState = contentState,
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt
index 977308178d..3dd964610a 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/JoinRoomView.kt
@@ -115,33 +115,33 @@ fun JoinRoomView(
contentState = state.contentState,
knockMessage = state.knockMessage,
hideAvatarsImages = state.hideAvatarsImages,
- onKnockMessageUpdate = { state.eventSink(JoinRoomEvents.UpdateKnockMessage(it)) },
+ onKnockMessageUpdate = { state.eventSink(JoinRoomEvent.UpdateKnockMessage(it)) },
)
},
footer = {
JoinRoomFooter(
joinAuthorisationStatus = state.joinAuthorisationStatus,
onAcceptInvite = { inviteData ->
- state.eventSink(JoinRoomEvents.AcceptInvite(inviteData))
+ state.eventSink(JoinRoomEvent.AcceptInvite(inviteData))
},
onDeclineInvite = { inviteData, blockUser ->
if (state.canReportRoom && blockUser) {
onDeclineInviteAndBlockUser(inviteData)
} else {
- state.eventSink(JoinRoomEvents.DeclineInvite(inviteData, blockUser = blockUser))
+ state.eventSink(JoinRoomEvent.DeclineInvite(inviteData, blockUser = blockUser))
}
},
onJoinRoom = {
- state.eventSink(JoinRoomEvents.JoinRoom)
+ state.eventSink(JoinRoomEvent.JoinRoom)
},
onKnockRoom = {
- state.eventSink(JoinRoomEvents.KnockRoom)
+ state.eventSink(JoinRoomEvent.KnockRoom)
},
onCancelKnock = {
- state.eventSink(JoinRoomEvents.CancelKnock(requiresConfirmation = true))
+ state.eventSink(JoinRoomEvent.CancelKnock(requiresConfirmation = true))
},
onForgetRoom = {
- state.eventSink(JoinRoomEvents.ForgetRoom)
+ state.eventSink(JoinRoomEvent.ForgetRoom)
},
onGoBack = onBackClick,
)
@@ -152,9 +152,9 @@ fun JoinRoomView(
RetryDialog(
title = stringResource(R.string.screen_join_room_loading_alert_title),
content = stringResource(CommonStrings.error_network_or_server_issue),
- onRetry = { state.eventSink(JoinRoomEvents.RetryFetchingContent) },
+ onRetry = { state.eventSink(JoinRoomEvent.RetryFetchingContent) },
onDismiss = {
- state.eventSink(JoinRoomEvents.DismissErrorAndHideContent)
+ state.eventSink(JoinRoomEvent.DismissErrorAndHideContent)
onBackClick()
}
)
@@ -166,7 +166,7 @@ fun JoinRoomView(
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
onSuccess = { onJoinSuccess() },
- onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
+ onErrorDismiss = { state.eventSink(JoinRoomEvent.ClearActionStates) },
)
}
AsyncActionView(
@@ -174,19 +174,19 @@ fun JoinRoomView(
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
onSuccess = { onKnockSuccess() },
- onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
+ onErrorDismiss = { state.eventSink(JoinRoomEvent.ClearActionStates) },
)
AsyncActionView(
async = state.forgetAction,
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
onSuccess = { onForgetSuccess() },
- onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
+ onErrorDismiss = { state.eventSink(JoinRoomEvent.ClearActionStates) },
)
AsyncActionView(
async = state.cancelKnockAction,
onSuccess = { onCancelKnockSuccess() },
- onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
+ onErrorDismiss = { state.eventSink(JoinRoomEvent.ClearActionStates) },
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
confirmationDialog = {
@@ -195,8 +195,8 @@ fun JoinRoomView(
title = stringResource(R.string.screen_join_room_cancel_knock_alert_title),
submitText = stringResource(R.string.screen_join_room_cancel_knock_alert_confirmation),
cancelText = stringResource(CommonStrings.action_no),
- onSubmitClick = { state.eventSink(JoinRoomEvents.CancelKnock(requiresConfirmation = false)) },
- onDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
+ onSubmitClick = { state.eventSink(JoinRoomEvent.CancelKnock(requiresConfirmation = false)) },
+ onDismiss = { state.eventSink(JoinRoomEvent.ClearActionStates) },
)
},
)
@@ -642,7 +642,7 @@ private fun JoinRoomTopBar(
@PreviewsDayNight
@Composable
-internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class) state: JoinRoomState) = ElementPreview {
+internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStatePreviewParam::class) state: JoinRoomState) = ElementPreview {
JoinRoomView(
state = state,
onBackClick = { },
diff --git a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomBindingContainer.kt
similarity index 98%
rename from features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt
rename to features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomBindingContainer.kt
index 3304169b1a..3ab9cbfa97 100644
--- a/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomModule.kt
+++ b/features/joinroom/impl/src/main/kotlin/io/element/android/features/joinroom/impl/di/JoinRoomBindingContainer.kt
@@ -27,7 +27,7 @@ import java.util.Optional
@BindingContainer
@ContributesTo(SessionScope::class)
-object JoinRoomModule {
+object JoinRoomBindingContainer {
@Provides
fun providesJoinRoomPresenterFactory(
client: MatrixClient,
diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt
index 41ed2cce40..3c104e47ae 100644
--- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt
+++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomPresenterTest.kt
@@ -12,7 +12,7 @@ import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.SeenInvitesStore
-import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
+import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
import io.element.android.features.invite.api.toInviteData
@@ -365,7 +365,7 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is invited then accept and decline events are sent to acceptDeclinePresenter`() = runTest {
- val eventSinkRecorder = lambdaRecorder { _: AcceptDeclineInviteEvents -> }
+ val eventSinkRecorder = lambdaRecorder { _: AcceptDeclineInviteEvent -> }
val acceptDeclinePresenter = Presenter {
anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
}
@@ -388,14 +388,14 @@ class JoinRoomPresenterTest {
skipItems(1)
awaitItem().also { state ->
- state.eventSink(JoinRoomEvents.AcceptInvite(inviteData))
- state.eventSink(JoinRoomEvents.DeclineInvite(inviteData, false))
+ state.eventSink(JoinRoomEvent.AcceptInvite(inviteData))
+ state.eventSink(JoinRoomEvent.DeclineInvite(inviteData, false))
assert(eventSinkRecorder)
.isCalledExactly(2)
.withSequence(
- listOf(value(AcceptDeclineInviteEvents.AcceptInvite(inviteData))),
- listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true))),
+ listOf(value(AcceptDeclineInviteEvent.AcceptInvite(inviteData))),
+ listOf(value(AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true))),
)
}
}
@@ -422,7 +422,7 @@ class JoinRoomPresenterTest {
presenter.test {
skipItems(1)
awaitItem().also { state ->
- state.eventSink(JoinRoomEvents.JoinRoom)
+ state.eventSink(JoinRoomEvent.JoinRoom)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Loading)
@@ -453,14 +453,14 @@ class JoinRoomPresenterTest {
presenter.test {
skipItems(1)
awaitItem().also { state ->
- state.eventSink(JoinRoomEvents.JoinRoom)
+ state.eventSink(JoinRoomEvent.JoinRoom)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Loading)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
- state.eventSink(JoinRoomEvents.ClearActionStates)
+ state.eventSink(JoinRoomEvent.ClearActionStates)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Uninitialized)
@@ -480,7 +480,7 @@ class JoinRoomPresenterTest {
presenter.test {
skipItems(1)
awaitItem().also { state ->
- state.eventSink(JoinRoomEvents.JoinRoom)
+ state.eventSink(JoinRoomEvent.JoinRoom)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Loading)
@@ -683,17 +683,17 @@ class JoinRoomPresenterTest {
presenter.test {
skipItems(1)
awaitItem().also { state ->
- state.eventSink(JoinRoomEvents.UpdateKnockMessage(knockMessage))
+ state.eventSink(JoinRoomEvent.UpdateKnockMessage(knockMessage))
}
awaitItem().also { state ->
- state.eventSink(JoinRoomEvents.KnockRoom)
+ state.eventSink(JoinRoomEvent.KnockRoom)
}
assertThat(awaitItem().knockAction).isEqualTo(AsyncAction.Loading)
awaitItem().also { state ->
assertThat(state.knockAction).isEqualTo(AsyncAction.Success(Unit))
fakeKnockRoom.lambda = knockRoomFailure
- state.eventSink(JoinRoomEvents.KnockRoom)
+ state.eventSink(JoinRoomEvent.KnockRoom)
}
assertThat(awaitItem().knockAction).isEqualTo(AsyncAction.Loading)
@@ -731,17 +731,17 @@ class JoinRoomPresenterTest {
presenter.test {
skipItems(1)
awaitItem().also { state ->
- state.eventSink(JoinRoomEvents.CancelKnock(true))
+ state.eventSink(JoinRoomEvent.CancelKnock(true))
}
awaitItem().also { state ->
assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.ConfirmingNoParams)
- state.eventSink(JoinRoomEvents.CancelKnock(false))
+ state.eventSink(JoinRoomEvent.CancelKnock(false))
}
assertThat(awaitItem().cancelKnockAction).isEqualTo(AsyncAction.Loading)
awaitItem().also { state ->
assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.Success(Unit))
cancelKnockRoom.lambda = cancelKnockRoomFailure
- state.eventSink(JoinRoomEvents.CancelKnock(false))
+ state.eventSink(JoinRoomEvent.CancelKnock(false))
}
assertThat(awaitItem().cancelKnockAction).isEqualTo(AsyncAction.Loading)
awaitItem().also { state ->
@@ -778,14 +778,14 @@ class JoinRoomPresenterTest {
presenter.test {
skipItems(1)
awaitItem().also { state ->
- state.eventSink(JoinRoomEvents.ForgetRoom)
+ state.eventSink(JoinRoomEvent.ForgetRoom)
}
assertThat(awaitItem().forgetAction).isEqualTo(AsyncAction.Loading)
awaitItem().also { state ->
assertThat(state.forgetAction).isEqualTo(AsyncAction.Success(Unit))
fakeForgetRoom.lambda = forgetRoomFailure
- state.eventSink(JoinRoomEvents.ForgetRoom)
+ state.eventSink(JoinRoomEvent.ForgetRoom)
}
assertThat(awaitItem().forgetAction).isEqualTo(AsyncAction.Loading)
diff --git a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt
index 0c510cacdb..c74e81010d 100644
--- a/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt
+++ b/features/joinroom/impl/src/test/kotlin/io/element/android/features/joinroom/impl/JoinRoomViewTest.kt
@@ -34,7 +34,7 @@ import org.junit.Test
class JoinRoomViewTest : RobolectricTest() {
@Test
fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder(expectEvents = false)
+ val eventsRecorder = EventsRecorder(expectEvents = false)
ensureCalledOnce {
setJoinRoomView(
aJoinRoomState(
@@ -48,7 +48,7 @@ class JoinRoomViewTest : RobolectricTest() {
@Test
fun `clicking on Join room on CanJoin room emits the expected Event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin),
@@ -56,12 +56,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_join_room_join_action)
- eventsRecorder.assertSingle(JoinRoomEvents.JoinRoom)
+ eventsRecorder.assertSingle(JoinRoomEvent.JoinRoom)
}
@Test
fun `clicking on Knock room on CanKnock room emits the expected Event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
@@ -70,12 +70,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_join_room_knock_action)
- eventsRecorder.assertSingle(JoinRoomEvents.KnockRoom)
+ eventsRecorder.assertSingle(JoinRoomEvent.KnockRoom)
}
@Test
fun `clicking on closing Knock error emits the expected Event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
@@ -84,12 +84,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_ok)
- eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
+ eventsRecorder.assertSingle(JoinRoomEvent.ClearActionStates)
}
@Test
fun `clicking on cancel knock request emit the expected Event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked),
@@ -97,12 +97,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_join_room_cancel_knock_action)
- eventsRecorder.assertSingle(JoinRoomEvents.CancelKnock(true))
+ eventsRecorder.assertSingle(JoinRoomEvent.CancelKnock(true))
}
@Test
fun `clicking on closing Cancel Knock error emits the expected Event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked),
@@ -111,12 +111,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_ok)
- eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
+ eventsRecorder.assertSingle(JoinRoomEvent.ClearActionStates)
}
@Test
fun `clicking on closing Join error emits the expected Event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
@@ -125,12 +125,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_ok)
- eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
+ eventsRecorder.assertSingle(JoinRoomEvent.ClearActionStates)
}
@Test
fun `when joining room is successful, the expected callback is invoked`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder(expectEvents = false)
+ val eventsRecorder = EventsRecorder(expectEvents = false)
ensureCalledOnce {
setJoinRoomView(
aJoinRoomState(
@@ -144,7 +144,7 @@ class JoinRoomViewTest : RobolectricTest() {
@Test
fun `clicking on Accept when JoinAuthorisationStatus is IsInvited emits the expected Event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
val inviteData = anInviteData()
setJoinRoomView(
aJoinRoomState(
@@ -153,12 +153,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_accept)
- eventsRecorder.assertSingle(JoinRoomEvents.AcceptInvite(inviteData))
+ eventsRecorder.assertSingle(JoinRoomEvent.AcceptInvite(inviteData))
}
@Test
fun `clicking on Decline when JoinAuthorisationStatus is IsInvited emits the expected Event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
val inviteData = anInviteData()
setJoinRoomView(
aJoinRoomState(
@@ -167,13 +167,13 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_decline)
- eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, false))
+ eventsRecorder.assertSingle(JoinRoomEvent.DeclineInvite(inviteData, false))
}
@Test
fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and can report room, the expected callback is invoked`() {
runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder(expectEvents = false)
+ val eventsRecorder = EventsRecorder(expectEvents = false)
val inviteData = anInviteData()
val joinRoomState = aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())),
@@ -192,7 +192,7 @@ class JoinRoomViewTest : RobolectricTest() {
@Test
fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and cant report room, emits the expected Event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
val inviteData = anInviteData()
val joinRoomState = aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())),
@@ -201,12 +201,12 @@ class JoinRoomViewTest : RobolectricTest() {
)
setJoinRoomView(state = joinRoomState)
clickOn(R.string.screen_join_room_decline_and_block_button_title)
- eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, true))
+ eventsRecorder.assertSingle(JoinRoomEvent.DeclineInvite(inviteData, true))
}
@Test
fun `clicking on Retry when an error occurs emits the expected Event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setJoinRoomView(
aJoinRoomState(
contentState = aFailureContentState(),
@@ -214,12 +214,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_retry)
- eventsRecorder.assertSingle(JoinRoomEvents.RetryFetchingContent)
+ eventsRecorder.assertSingle(JoinRoomEvent.RetryFetchingContent)
}
@Test
fun `clicking on ok when user is unauthorized the expected callback`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder(expectEvents = false)
+ val eventsRecorder = EventsRecorder(expectEvents = false)
ensureCalledOnce {
setJoinRoomView(
aJoinRoomState(
@@ -235,7 +235,7 @@ class JoinRoomViewTest : RobolectricTest() {
@Test
fun `clicking on forget when user is banned invokes the expected callback`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(null, null)),
@@ -243,7 +243,7 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_join_room_forget_action)
- eventsRecorder.assertSingle(JoinRoomEvents.ForgetRoom)
+ eventsRecorder.assertSingle(JoinRoomEvent.ForgetRoom)
}
}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerEvents.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerEvent.kt
similarity index 65%
rename from features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerEvents.kt
rename to features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerEvent.kt
index 740982b612..b8cd9345c2 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerEvents.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerEvent.kt
@@ -8,7 +8,7 @@
package io.element.android.features.knockrequests.impl.banner
-sealed interface KnockRequestsBannerEvents {
- data object AcceptSingleRequest : KnockRequestsBannerEvents
- data object Dismiss : KnockRequestsBannerEvents
+sealed interface KnockRequestsBannerEvent {
+ data object AcceptSingleRequest : KnockRequestsBannerEvent
+ data object Dismiss : KnockRequestsBannerEvent
}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt
index f340d597b1..a96b6d3de3 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt
@@ -53,15 +53,15 @@ class KnockRequestsBannerPresenter(
}
}
- fun handleEvent(event: KnockRequestsBannerEvents) {
+ fun handleEvent(event: KnockRequestsBannerEvent) {
when (event) {
- is KnockRequestsBannerEvents.AcceptSingleRequest -> {
+ is KnockRequestsBannerEvent.AcceptSingleRequest -> {
sessionCoroutineScope.acceptSingleKnockRequest(
knockRequests = knockRequests,
displayAcceptError = showAcceptError,
)
}
- is KnockRequestsBannerEvents.Dismiss -> {
+ is KnockRequestsBannerEvent.Dismiss -> {
sessionCoroutineScope.launch {
knockRequestsService.markAllKnockRequestsAsSeen()
}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt
index e9291b501a..2b6532c355 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt
@@ -20,7 +20,7 @@ data class KnockRequestsBannerState(
val knockRequests: ImmutableList,
val displayAcceptError: Boolean,
val canAccept: Boolean,
- val eventSink: (KnockRequestsBannerEvents) -> Unit,
+ val eventSink: (KnockRequestsBannerEvent) -> Unit,
) {
val subtitle = knockRequests.singleOrNull()?.userId?.value
val reason = knockRequests.singleOrNull()?.reason
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStatePreviewParam.kt
similarity index 95%
rename from features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt
rename to features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStatePreviewParam.kt
index 7210e783fe..8df5e08985 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStatePreviewParam.kt
@@ -16,7 +16,7 @@ import io.element.android.libraries.designsystem.preview.USER_NAME_BOB
import io.element.android.libraries.designsystem.preview.USER_NAME_CHARLIE
import kotlinx.collections.immutable.toImmutableList
-class KnockRequestsBannerStateProvider : PreviewParameterProvider {
+class KnockRequestsBannerStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aKnockRequestsBannerState(),
@@ -64,7 +64,7 @@ fun aKnockRequestsBannerState(
displayAcceptError: Boolean = false,
canAccept: Boolean = true,
isVisible: Boolean = true,
- eventSink: (KnockRequestsBannerEvents) -> Unit = {}
+ eventSink: (KnockRequestsBannerEvent) -> Unit = {}
) = KnockRequestsBannerState(
knockRequests = knockRequests.toImmutableList(),
displayAcceptError = displayAcceptError,
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt
index 431bd14d10..329e7a2a56 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt
@@ -109,11 +109,11 @@ private fun KnockRequestsBannerContent(
modifier: Modifier = Modifier,
) {
fun onDismissClick() {
- state.eventSink(KnockRequestsBannerEvents.Dismiss)
+ state.eventSink(KnockRequestsBannerEvent.Dismiss)
}
fun onAcceptClick() {
- state.eventSink(KnockRequestsBannerEvents.AcceptSingleRequest)
+ state.eventSink(KnockRequestsBannerEvent.AcceptSingleRequest)
}
Column(
@@ -227,7 +227,7 @@ private fun KnockRequestAvatarListView(
@Composable
@PreviewsDayNight
-internal fun KnockRequestsBannerViewPreview(@PreviewParameter(KnockRequestsBannerStateProvider::class) state: KnockRequestsBannerState) = ElementPreview {
+internal fun KnockRequestsBannerViewPreview(@PreviewParameter(KnockRequestsBannerStatePreviewParam::class) state: KnockRequestsBannerState) = ElementPreview {
KnockRequestsBannerView(
state = state,
onViewRequestsClick = {},
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/di/KnockRequestsBindingContainer.kt
similarity index 81%
rename from features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt
rename to features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/di/KnockRequestsBindingContainer.kt
index 73e4a2ddeb..04d5400831 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/di/KnockRequestsBindingContainer.kt
@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.features.knockrequests.impl.data
+package io.element.android.features.knockrequests.impl.data.di
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.ContributesTo
@@ -14,16 +14,17 @@ import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn
import io.element.android.features.knockrequests.api.KnockRequestPermissions
import io.element.android.features.knockrequests.api.knockRequestPermissions
+import io.element.android.features.knockrequests.impl.data.KnockRequestsService
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.powerlevels.permissionsFlow
@BindingContainer
@ContributesTo(RoomScope::class)
-object KnockRequestsModule {
+object KnockRequestsBindingContainer {
@Provides
@SingleIn(RoomScope::class)
- fun knockRequestsService(room: JoinedRoom): KnockRequestsService {
+ fun providesKnockRequestsService(room: JoinedRoom): KnockRequestsService {
return KnockRequestsService(
knockRequestsFlow = room.knockRequestsFlow,
permissionsFlow = room.permissionsFlow(KnockRequestPermissions.DEFAULT) { perms ->
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListEvents.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListEvent.kt
similarity index 61%
rename from features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListEvents.kt
rename to features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListEvent.kt
index f1350b1669..6b9a6ac1a8 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListEvents.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListEvent.kt
@@ -10,12 +10,12 @@ package io.element.android.features.knockrequests.impl.list
import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
-sealed interface KnockRequestsListEvents {
- data class Accept(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvents
- data class Decline(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvents
- data class DeclineAndBan(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvents
- data object AcceptAll : KnockRequestsListEvents
- data object ResetCurrentAction : KnockRequestsListEvents
- data object RetryCurrentAction : KnockRequestsListEvents
- data object ConfirmCurrentAction : KnockRequestsListEvents
+sealed interface KnockRequestsListEvent {
+ data class Accept(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvent
+ data class Decline(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvent
+ data class DeclineAndBan(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvent
+ data object AcceptAll : KnockRequestsListEvent
+ data object ResetCurrentAction : KnockRequestsListEvent
+ data object RetryCurrentAction : KnockRequestsListEvent
+ data object ConfirmCurrentAction : KnockRequestsListEvent
}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt
index a56dc40731..76b562d8b3 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt
@@ -39,28 +39,28 @@ class KnockRequestsListPresenter(
val coroutineScope = rememberCoroutineScope()
- fun handleEvent(event: KnockRequestsListEvents) {
+ fun handleEvent(event: KnockRequestsListEvent) {
when (event) {
- KnockRequestsListEvents.AcceptAll -> {
+ KnockRequestsListEvent.AcceptAll -> {
currentAction = KnockRequestsAction.AcceptAll
}
- is KnockRequestsListEvents.Accept -> {
+ is KnockRequestsListEvent.Accept -> {
currentAction = KnockRequestsAction.Accept(event.knockRequest)
}
- is KnockRequestsListEvents.Decline -> {
+ is KnockRequestsListEvent.Decline -> {
currentAction = KnockRequestsAction.Decline(event.knockRequest)
}
- is KnockRequestsListEvents.DeclineAndBan -> {
+ is KnockRequestsListEvent.DeclineAndBan -> {
currentAction = KnockRequestsAction.DeclineAndBan(event.knockRequest)
}
- KnockRequestsListEvents.ResetCurrentAction -> {
+ KnockRequestsListEvent.ResetCurrentAction -> {
asyncAction.value = AsyncAction.Uninitialized
currentAction = KnockRequestsAction.None
}
- KnockRequestsListEvents.RetryCurrentAction -> {
+ KnockRequestsListEvent.RetryCurrentAction -> {
coroutineScope.executeAction(currentAction, asyncAction, isActionConfirmed = true)
}
- KnockRequestsListEvents.ConfirmCurrentAction -> {
+ KnockRequestsListEvent.ConfirmCurrentAction -> {
coroutineScope.executeAction(currentAction, asyncAction, isActionConfirmed = true)
}
}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt
index ae770a297d..0a0e576eae 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt
@@ -20,7 +20,7 @@ data class KnockRequestsListState(
val currentAction: KnockRequestsAction,
val asyncAction: AsyncAction,
val permissions: KnockRequestPermissions,
- val eventSink: (KnockRequestsListEvents) -> Unit,
+ val eventSink: (KnockRequestsListEvent) -> Unit,
) {
val canAcceptAll = permissions.canAccept && knockRequests is AsyncData.Success && knockRequests.data.size > 1
}
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStatePreviewParam.kt
similarity index 97%
rename from features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt
rename to features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStatePreviewParam.kt
index 85fc0675ad..691d874af6 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStatePreviewParam.kt
@@ -18,7 +18,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
-open class KnockRequestsListStateProvider : PreviewParameterProvider {
+open class KnockRequestsListStatePreviewParam : PreviewParameterProvider {
override val values: Sequence
get() = sequenceOf(
aKnockRequestsListState(
@@ -140,7 +140,7 @@ fun aKnockRequestsListState(
canDecline = true,
canBan = true,
),
- eventSink: (KnockRequestsListEvents) -> Unit = {},
+ eventSink: (KnockRequestsListEvent) -> Unit = {},
) = KnockRequestsListState(
knockRequests = knockRequests,
currentAction = currentAction,
diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt
index db40596892..52b11bc8c4 100644
--- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt
+++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt
@@ -108,15 +108,15 @@ private fun KnockRequestsListContent(
modifier: Modifier = Modifier,
) {
fun onAcceptClick(knockRequest: KnockRequestPresentable) {
- state.eventSink(KnockRequestsListEvents.Accept(knockRequest))
+ state.eventSink(KnockRequestsListEvent.Accept(knockRequest))
}
fun onDeclineClick(knockRequest: KnockRequestPresentable) {
- state.eventSink(KnockRequestsListEvents.Decline(knockRequest))
+ state.eventSink(KnockRequestsListEvent.Decline(knockRequest))
}
fun onBanClick(knockRequest: KnockRequestPresentable) {
- state.eventSink(KnockRequestsListEvents.DeclineAndBan(knockRequest))
+ state.eventSink(KnockRequestsListEvent.DeclineAndBan(knockRequest))
}
var bottomPaddingInPixels by remember { mutableIntStateOf(0) }
@@ -160,19 +160,19 @@ private fun KnockRequestsListContent(
currentAction = state.currentAction,
asyncAction = state.asyncAction,
onConfirm = {
- state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
+ state.eventSink(KnockRequestsListEvent.ConfirmCurrentAction)
},
onRetry = {
- state.eventSink(KnockRequestsListEvents.RetryCurrentAction)
+ state.eventSink(KnockRequestsListEvent.RetryCurrentAction)
},
onDismiss = {
- state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
+ state.eventSink(KnockRequestsListEvent.ResetCurrentAction)
},
)
if (state.canAcceptAll) {
KnockRequestsAcceptAll(
onClick = {
- state.eventSink(KnockRequestsListEvents.AcceptAll)
+ state.eventSink(KnockRequestsListEvent.AcceptAll)
},
onHeightChange = { height ->
bottomPaddingInPixels = height
@@ -491,7 +491,7 @@ private fun KnockRequestsListTopBar(onBackClick: () -> Unit) {
@PreviewsDayNight
@Composable
internal fun KnockRequestsListViewPreview(
- @PreviewParameter(KnockRequestsListStateProvider::class) state: KnockRequestsListState
+ @PreviewParameter(KnockRequestsListStatePreviewParam::class) state: KnockRequestsListState
) = ElementPreview {
KnockRequestsListView(
state = state,
diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt
index 1595fcdbd9..2c1a185ba2 100644
--- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt
+++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt
@@ -146,7 +146,7 @@ import org.junit.Test
presenter.test {
skipItems(2)
awaitItem().also { state ->
- state.eventSink(KnockRequestsBannerEvents.AcceptSingleRequest)
+ state.eventSink(KnockRequestsBannerEvent.AcceptSingleRequest)
}
awaitItem().also { state ->
assertThat(state.isVisible).isFalse()
@@ -182,7 +182,7 @@ import org.junit.Test
skipItems(2)
awaitItem().also { state ->
assertThat(state.knockRequests).hasSize(1)
- state.eventSink(KnockRequestsBannerEvents.AcceptSingleRequest)
+ state.eventSink(KnockRequestsBannerEvent.AcceptSingleRequest)
}
awaitItem().also { state ->
assertThat(state.isVisible).isFalse()
@@ -206,7 +206,7 @@ import org.junit.Test
presenter.test {
skipItems(2)
awaitItem().also { state ->
- state.eventSink(KnockRequestsBannerEvents.Dismiss)
+ state.eventSink(KnockRequestsBannerEvent.Dismiss)
}
advanceUntilIdle()
assert(markAsSeenLambda).isCalledExactly(3)
diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt
index f55ccb0cef..441b782a4a 100644
--- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt
+++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt
@@ -29,7 +29,7 @@ import org.junit.Test
class KnockRequestsBannerViewTest : RobolectricTest() {
@Test
fun `clicking on view on single request invoke the expected callback`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder(expectEvents = false)
+ val eventsRecorder = EventsRecorder(expectEvents = false)
ensureCalledOnce {
setKnockRequestsBannerView(
state = aKnockRequestsBannerState(
@@ -43,7 +43,7 @@ class KnockRequestsBannerViewTest : RobolectricTest() {
@Test
fun `clicking on view all when multiple requests invoke the expected callback`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder(expectEvents = false)
+ val eventsRecorder = EventsRecorder(expectEvents = false)
ensureCalledOnce {
setKnockRequestsBannerView(
state = aKnockRequestsBannerState(
@@ -62,19 +62,19 @@ class KnockRequestsBannerViewTest : RobolectricTest() {
@Test
fun `clicking on accept on a single request emit the expected event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setKnockRequestsBannerView(
state = aKnockRequestsBannerState(
eventSink = eventsRecorder,
),
)
clickOn(CommonStrings.action_accept)
- eventsRecorder.assertSingle(KnockRequestsBannerEvents.AcceptSingleRequest)
+ eventsRecorder.assertSingle(KnockRequestsBannerEvent.AcceptSingleRequest)
}
@Test
fun `clicking on dismiss emit the expected event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
setKnockRequestsBannerView(
state = aKnockRequestsBannerState(
eventSink = eventsRecorder,
@@ -82,7 +82,7 @@ class KnockRequestsBannerViewTest : RobolectricTest() {
)
val close = activity!!.getString(CommonStrings.action_close)
onNodeWithContentDescription(close).performClick()
- eventsRecorder.assertSingle(KnockRequestsBannerEvents.Dismiss)
+ eventsRecorder.assertSingle(KnockRequestsBannerEvent.Dismiss)
}
}
diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt
index 209e67cadf..dc04889d32 100644
--- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt
+++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt
@@ -65,7 +65,7 @@ class KnockRequestsListPresenterTest {
skipItems(2)
awaitItem().also { state ->
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
- state.eventSink(KnockRequestsListEvents.Accept(knockRequestPresentable))
+ state.eventSink(KnockRequestsListEvent.Accept(knockRequestPresentable))
}
skipItems(1)
awaitItem().also { state ->
@@ -75,7 +75,7 @@ class KnockRequestsListPresenterTest {
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java)
- state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
+ state.eventSink(KnockRequestsListEvent.ResetCurrentAction)
}
skipItems(2)
awaitItem().also { state ->
@@ -99,7 +99,7 @@ class KnockRequestsListPresenterTest {
skipItems(2)
awaitItem().also { state ->
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
- state.eventSink(KnockRequestsListEvents.Accept(knockRequestPresentable))
+ state.eventSink(KnockRequestsListEvent.Accept(knockRequestPresentable))
}
skipItems(1)
awaitItem().also { state ->
@@ -109,14 +109,14 @@ class KnockRequestsListPresenterTest {
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Failure::class.java)
- state.eventSink(KnockRequestsListEvents.RetryCurrentAction)
+ state.eventSink(KnockRequestsListEvent.RetryCurrentAction)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Failure::class.java)
- state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
+ state.eventSink(KnockRequestsListEvent.ResetCurrentAction)
}
skipItems(1)
awaitItem().also { state ->
@@ -140,21 +140,21 @@ class KnockRequestsListPresenterTest {
skipItems(2)
awaitItem().also { state ->
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
- state.eventSink(KnockRequestsListEvents.Decline(knockRequestPresentable))
+ state.eventSink(KnockRequestsListEvent.Decline(knockRequestPresentable))
}
skipItems(1)
awaitItem().also { state ->
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
assertThat(state.currentAction).isEqualTo(KnockRequestsAction.Decline(knockRequestPresentable))
assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java)
- state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
+ state.eventSink(KnockRequestsListEvent.ConfirmCurrentAction)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java)
- state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
+ state.eventSink(KnockRequestsListEvent.ResetCurrentAction)
}
skipItems(2)
awaitItem().also { state ->
@@ -178,21 +178,21 @@ class KnockRequestsListPresenterTest {
skipItems(2)
awaitItem().also { state ->
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
- state.eventSink(KnockRequestsListEvents.DeclineAndBan(knockRequestPresentable))
+ state.eventSink(KnockRequestsListEvent.DeclineAndBan(knockRequestPresentable))
}
skipItems(1)
awaitItem().also { state ->
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
assertThat(state.currentAction).isEqualTo(KnockRequestsAction.DeclineAndBan(knockRequestPresentable))
assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java)
- state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
+ state.eventSink(KnockRequestsListEvent.ConfirmCurrentAction)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java)
- state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
+ state.eventSink(KnockRequestsListEvent.ResetCurrentAction)
}
skipItems(2)
awaitItem().also { state ->
@@ -220,20 +220,20 @@ class KnockRequestsListPresenterTest {
skipItems(2)
awaitItem().also { state ->
assertThat(state.canAcceptAll).isTrue()
- state.eventSink(KnockRequestsListEvents.AcceptAll)
+ state.eventSink(KnockRequestsListEvent.AcceptAll)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.currentAction).isEqualTo(KnockRequestsAction.AcceptAll)
assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java)
- state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
+ state.eventSink(KnockRequestsListEvent.ConfirmCurrentAction)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java)
- state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
+ state.eventSink(KnockRequestsListEvent.ResetCurrentAction)
}
skipItems(2)
awaitItem().also { state ->
@@ -262,20 +262,20 @@ class KnockRequestsListPresenterTest {
skipItems(2)
awaitItem().also { state ->
assertThat(state.canAcceptAll).isTrue()
- state.eventSink(KnockRequestsListEvents.AcceptAll)
+ state.eventSink(KnockRequestsListEvent.AcceptAll)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.currentAction).isEqualTo(KnockRequestsAction.AcceptAll)
assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java)
- state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
+ state.eventSink(KnockRequestsListEvent.ConfirmCurrentAction)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Failure::class.java)
- state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
+ state.eventSink(KnockRequestsListEvent.ResetCurrentAction)
}
skipItems(2)
awaitItem().also { state ->
diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt
index 1fbcb67b21..c1caaddb1e 100644
--- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt
+++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt
@@ -31,7 +31,7 @@ import org.junit.Test
class KnockRequestsListViewTest : RobolectricTest() {
@Test
fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder(expectEvents = false)
+ val eventsRecorder = EventsRecorder(expectEvents = false)
ensureCalledOnce {
setKnockRequestsListView(
aKnockRequestsListState(
@@ -45,7 +45,7 @@ class KnockRequestsListViewTest : RobolectricTest() {
@Test
fun `clicking on accept emit the expected event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder()
val knockRequest = aKnockRequestPresentable()
setKnockRequestsListView(
aKnockRequestsListState(
@@ -54,12 +54,12 @@ class KnockRequestsListViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_accept)
- eventsRecorder.assertSingle(KnockRequestsListEvents.Accept(knockRequest))
+ eventsRecorder.assertSingle(KnockRequestsListEvent.Accept(knockRequest))
}
@Test
fun `clicking on decline emit the expected event`() = runAndroidComposeUiTest {
- val eventsRecorder = EventsRecorder()
+ val eventsRecorder = EventsRecorder