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() val knockRequest = aKnockRequestPresentable() setKnockRequestsListView( aKnockRequestsListState( @@ -68,12 +68,12 @@ class KnockRequestsListViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_decline) - eventsRecorder.assertSingle(KnockRequestsListEvents.Decline(knockRequest)) + eventsRecorder.assertSingle(KnockRequestsListEvent.Decline(knockRequest)) } @Test fun `clicking on decline and ban emit the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() val knockRequest = aKnockRequestPresentable() setKnockRequestsListView( aKnockRequestsListState( @@ -82,12 +82,12 @@ class KnockRequestsListViewTest : RobolectricTest() { ), ) clickOn(R.string.screen_knock_requests_list_decline_and_ban_action_title) - eventsRecorder.assertSingle(KnockRequestsListEvents.DeclineAndBan(knockRequest)) + eventsRecorder.assertSingle(KnockRequestsListEvent.DeclineAndBan(knockRequest)) } @Test fun `clicking on accept all emit the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) setKnockRequestsListView( aKnockRequestsListState( @@ -96,12 +96,12 @@ class KnockRequestsListViewTest : RobolectricTest() { ), ) clickOn(R.string.screen_knock_requests_list_accept_all_button_title) - eventsRecorder.assertSingle(KnockRequestsListEvents.AcceptAll) + eventsRecorder.assertSingle(KnockRequestsListEvent.AcceptAll) } @Test fun `retry on async view retry emit the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) setKnockRequestsListView( aKnockRequestsListState( @@ -112,12 +112,12 @@ class KnockRequestsListViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_retry) - eventsRecorder.assertSingle(KnockRequestsListEvents.RetryCurrentAction) + eventsRecorder.assertSingle(KnockRequestsListEvent.RetryCurrentAction) } @Test fun `canceling async view emit the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) setKnockRequestsListView( aKnockRequestsListState( @@ -128,12 +128,12 @@ class KnockRequestsListViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_cancel) - eventsRecorder.assertSingle(KnockRequestsListEvents.ResetCurrentAction) + eventsRecorder.assertSingle(KnockRequestsListEvent.ResetCurrentAction) } @Test fun `confirming async view emit the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) setKnockRequestsListView( aKnockRequestsListState( @@ -144,7 +144,7 @@ class KnockRequestsListViewTest : RobolectricTest() { ), ) clickOn(R.string.screen_knock_requests_list_accept_all_alert_confirm_button_title) - eventsRecorder.assertSingle(KnockRequestsListEvents.ConfirmCurrentAction) + eventsRecorder.assertSingle(KnockRequestsListEvent.ConfirmCurrentAction) } } diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomStateProvider.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomStatePreviewParam.kt similarity index 94% rename from features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomStateProvider.kt rename to features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomStatePreviewParam.kt index 769015f301..5f68a6f192 100644 --- a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomStateProvider.kt +++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/InternalLeaveRoomStatePreviewParam.kt @@ -13,7 +13,7 @@ import io.element.android.features.leaveroom.api.LeaveRoomEvent import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.core.RoomId -class InternalLeaveRoomStateProvider : PreviewParameterProvider { +class InternalLeaveRoomStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aLeaveRoomState(), diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt index d11dc7e7e8..e2065bca1f 100644 --- a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt +++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenter.kt @@ -60,7 +60,13 @@ class LeaveRoomPresenter( roomId: RoomId, leaveAction: MutableState>, ) = launch(dispatchers.io) { - client.getRoom(roomId)?.use { room -> + val room = client.getRoom(roomId) + if (room == null) { + Timber.e("Unable to find room $roomId to leave it") + leaveAction.value = AsyncAction.Failure(RoomNotFoundException(roomId)) + return@launch + } + room.use { val roomInfo = room.roomInfoFlow.first() leaveAction.value = when { roomInfo.isDm -> Confirmation.Dm(roomId) @@ -78,7 +84,8 @@ class LeaveRoomPresenter( leaveAction: MutableState>, ) = launch(dispatchers.io) { leaveAction.runCatchingUpdatingState { - client.getRoom(roomId)!!.use { room -> + val room = client.getRoom(roomId) ?: throw RoomNotFoundException(roomId) + room.use { room .leave() .onSuccess { notificationConversationService.onLeftRoom(client.sessionId, roomId) } @@ -100,3 +107,5 @@ class LeaveRoomPresenter( } } } + +internal class RoomNotFoundException(roomId: RoomId) : IllegalStateException("Room $roomId not found") diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomView.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomView.kt index 131801b014..c3a916b141 100644 --- a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomView.kt +++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomView.kt @@ -139,7 +139,7 @@ private fun LeaveRoomProgressDialog(modifier: Modifier = Modifier) { @PreviewsDayNight @Composable internal fun LeaveRoomViewPreview( - @PreviewParameter(InternalLeaveRoomStateProvider::class) state: InternalLeaveRoomState + @PreviewParameter(InternalLeaveRoomStatePreviewParam::class) state: InternalLeaveRoomState ) = ElementPreview { Box( modifier = Modifier.size(300.dp, 300.dp), diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/di/LeaveRoomModule.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/di/LeaveRoomBindingContainer.kt similarity index 95% rename from features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/di/LeaveRoomModule.kt rename to features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/di/LeaveRoomBindingContainer.kt index db5b1416ee..866af68cc8 100644 --- a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/di/LeaveRoomModule.kt +++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/di/LeaveRoomBindingContainer.kt @@ -18,7 +18,7 @@ import io.element.android.libraries.di.SessionScope @ContributesTo(SessionScope::class) @BindingContainer -interface LeaveRoomModule { +interface LeaveRoomBindingContainer { @Binds fun bindLeaveRoomPresenter(presenter: LeaveRoomPresenter): Presenter } diff --git a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveBaseRoomPresenterTest.kt b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveBaseRoomPresenterTest.kt index 90b7f2369f..f8f7a771e1 100644 --- a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveBaseRoomPresenterTest.kt +++ b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveBaseRoomPresenterTest.kt @@ -175,6 +175,32 @@ class LeaveBaseRoomPresenterTest { } } + @Test + fun `present - show error if the room to confirm leaving is not found`() = runTest { + val presenter = createLeaveRoomPresenter(client = FakeMatrixClient()) + presenter.stateFlow().test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = true)) + val errorState = awaitItem() + assertThat(errorState.leaveAction).isInstanceOf(AsyncAction.Failure::class.java) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - show error if the room to leave is not found`() = runTest { + val presenter = createLeaveRoomPresenter(client = FakeMatrixClient()) + presenter.stateFlow().test { + val initialState = awaitItem() + initialState.eventSink(LeaveRoomEvent.LeaveRoom(A_ROOM_ID, needsConfirmation = false)) + val progressState = awaitItem() + assertThat(progressState.leaveAction).isEqualTo(AsyncAction.Loading) + val errorState = awaitItem() + assertThat(errorState.leaveAction).isInstanceOf(AsyncAction.Failure::class.java) + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `present - reset state after error`() = runTest { val presenter = createLeaveRoomPresenter( diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStateProvider.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStatePreviewParam.kt similarity index 95% rename from features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStateProvider.kt rename to features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStatePreviewParam.kt index 6a74cc01eb..b984c43398 100644 --- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStateProvider.kt +++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListStatePreviewParam.kt @@ -15,7 +15,7 @@ import io.element.android.libraries.architecture.AsyncData import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -open class DependencyLicensesListStateProvider : PreviewParameterProvider { +open class DependencyLicensesListStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aDependencyLicensesListState( diff --git a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt index 943f57ab3b..17f82cd14e 100644 --- a/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt +++ b/features/licenses/impl/src/main/kotlin/io/element/android/features/licenses/impl/list/DependencyLicensesListView.kt @@ -126,7 +126,7 @@ fun DependencyLicensesListView( @PreviewsDayNight @Composable internal fun DependencyLicensesListViewPreview( - @PreviewParameter(DependencyLicensesListStateProvider::class) state: DependencyLicensesListState + @PreviewParameter(DependencyLicensesListStatePreviewParam::class) state: DependencyLicensesListState ) = ElementPreview { DependencyLicensesListView( state = state, diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDesktopHandler.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDesktopHandler.kt index 7afb9a7623..bff81a3e9e 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDesktopHandler.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDesktopHandler.kt @@ -39,6 +39,10 @@ class LinkNewDesktopHandler( LinkDesktopStep.Uninitialized ) + /** + * A [StateFlow] that emits the current step of the link desktop process. + * It starts with [LinkDesktopStep.Uninitialized] and will emit other steps as they occur. + */ val stepFlow: StateFlow get() = linkDesktopStepFlow.asStateFlow() @@ -53,15 +57,31 @@ class LinkNewDesktopHandler( timerJob = null } - fun createNewHandler() { - resetJobs() - handler = matrixClient.createLinkDesktopHandler().getOrNull() + /** + * Starts (or restarts) a timer that will emit an error in [stepFlow] after 2 minutes. + */ + fun startTimer() { + timerJob?.cancel() timerJob = sessionScope.launch { delay(2.minutes) linkDesktopStepFlow.emit(LinkDesktopStep.Error(ErrorType.Expired("Scanning QrCode took too long."))) } } + /** + * Creates a new [LinkDesktopHandler] and cancels any existing job. + * This should be called when the user wants to retry scanning a QR code. + */ + fun createNewHandler() { + currentJob?.cancel() + currentJob = null + handler = matrixClient.createLinkDesktopHandler().getOrNull() + } + + /** + * Resets the state of the handler and cancels any existing job. + * This should be called when the user wants to start over. + */ fun reset() { resetJobs() sessionScope.launch { @@ -69,6 +89,12 @@ class LinkNewDesktopHandler( } } + /** + * Handles the scanned QR code data. + * This should be called when the user scans a QR code. + * + * @param data The scanned QR code data as a byte array. + */ fun onScannedCode(data: ByteArray) { resetJobs() val currentHandler = handler diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDeviceFlowNode.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDeviceFlowNode.kt index afa570f0a9..b31aca8cb2 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDeviceFlowNode.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewDeviceFlowNode.kt @@ -263,6 +263,8 @@ class LinkNewDeviceFlowNode( } LinkDeviceType.Desktop -> { linkNewDesktopHandler.reset() + // Ensure unlock state does not last longer than the timeout for scanning the QR code, so start the timer after unlock + linkNewDesktopHandler.startTimer() backstack.push(NavTarget.DesktopNotice) } } @@ -278,10 +280,14 @@ class LinkNewDeviceFlowNode( NavTarget.DesktopNotice -> { val callback = object : DesktopNoticeNode.Callback { override fun navigateBack() { + // Stop the timer when the user leaves the screen (user will have to authenticate again) + linkNewDesktopHandler.reset() backstack.pop() } override fun navigateToQrCodeScanner() { + // Start again the timer when the user enters the next screen (they clicked on "Ready to scan") + linkNewDesktopHandler.startTimer() backstack.push(NavTarget.DesktopScanQrCode) } } @@ -290,6 +296,8 @@ class LinkNewDeviceFlowNode( NavTarget.DesktopScanQrCode -> { val callback = object : ScanQrCodeNode.Callback { override fun cancel() { + // start again the timer when the user goes back to the DesktopNoticeNode + linkNewDesktopHandler.startTimer() backstack.pop() } } diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewMobileHandler.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewMobileHandler.kt index 39c9c6fef3..9436c0e901 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewMobileHandler.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/LinkNewMobileHandler.kt @@ -40,9 +40,19 @@ class LinkNewMobileHandler( LinkMobileStep.Uninitialized ) + /** + * A [StateFlow] that emits the current step of the link mobile process. + * It starts with [LinkMobileStep.Uninitialized] and will emit other steps as they occur. + */ val stepFlow: StateFlow get() = linkMobileStepFlow.asStateFlow() + /** + * Creates a new [LinkMobileHandler] and starts the linking process. + * If [forRotating] is true, it indicates that this is a rotation of the QR code. + * + * @param forRotating Whether this is a rotation of the QR code. Defaults to false. + */ fun createAndStartNewHandler(forRotating: Boolean = false) { Timber.tag(loggerTag.value).d("createAndStartNewHandler()") currentJob?.cancel() @@ -62,6 +72,10 @@ class LinkNewMobileHandler( } } + /** + * Resets the handler and cancels any ongoing job. + * This will also emit [LinkMobileStep.Uninitialized] to the [stepFlow]. + */ fun reset() { currentJob?.cancel() currentJob = null @@ -70,10 +84,18 @@ class LinkNewMobileHandler( } } + /** + * Rotates the QR code by creating a new handler and starting the linking process. + * This is typically called when a timeout occurs for the current QR code. + */ fun rotateQrCode() { createAndStartNewHandler(forRotating = true) } + /** + * Handles the case where there are too many QR code rotations. + * This will reset the handler and emit an error to the [stepFlow]. + */ fun onTooManyRotation() { reset() sessionScope.launch { diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeStateProvider.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeStatePreviewParam.kt similarity index 92% rename from features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeStateProvider.kt rename to features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeStatePreviewParam.kt index 194bd6fafc..c539563fc7 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeStateProvider.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeStatePreviewParam.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.permissions.api.PermissionsState import io.element.android.libraries.permissions.api.aPermissionsState -open class DesktopNoticeStateProvider : PreviewParameterProvider { +open class DesktopNoticeStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aDesktopNoticeState(), diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeView.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeView.kt index a66ea7b391..935c848e42 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeView.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/desktop/DesktopNoticeView.kt @@ -118,7 +118,7 @@ fun DesktopNoticeView( @PreviewsDayNight @Composable internal fun DesktopNoticeViewPreview( - @PreviewParameter(DesktopNoticeStateProvider::class) state: DesktopNoticeState, + @PreviewParameter(DesktopNoticeStatePreviewParam::class) state: DesktopNoticeState, ) = ElementPreview { DesktopNoticeView( state = state, diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorScreenTypeProvider.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorScreenTypePreviewParam.kt similarity index 90% rename from features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorScreenTypeProvider.kt rename to features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorScreenTypePreviewParam.kt index 5946eb9ab2..bfaf511e66 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorScreenTypeProvider.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorScreenTypePreviewParam.kt @@ -9,7 +9,7 @@ package io.element.android.features.linknewdevice.impl.screens.error import androidx.compose.ui.tooling.preview.PreviewParameterProvider -class ErrorScreenTypeProvider : PreviewParameterProvider { +class ErrorScreenTypePreviewParam : PreviewParameterProvider { override val values: Sequence = sequenceOf( ErrorScreenType.Cancelled, ErrorScreenType.Declined, diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorView.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorView.kt index 7b830626f6..90e9c4616b 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorView.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/error/ErrorView.kt @@ -165,7 +165,7 @@ private fun ColumnScope.Buttons( @PreviewsDayNight @Composable -internal fun ErrorViewPreview(@PreviewParameter(ErrorScreenTypeProvider::class) errorScreenType: ErrorScreenType) { +internal fun ErrorViewPreview(@PreviewParameter(ErrorScreenTypePreviewParam::class) errorScreenType: ErrorScreenType) { ElementPreview { ErrorView( errorScreenType = errorScreenType, diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberStateProvider.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberStatePreviewParam.kt similarity index 93% rename from features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberStateProvider.kt rename to features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberStatePreviewParam.kt index 126bfeee52..56071f3e8f 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberStateProvider.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberStatePreviewParam.kt @@ -11,7 +11,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.linknewdevice.ErrorType -open class EnterNumberStateProvider : PreviewParameterProvider { +open class EnterNumberStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aEnterNumberState(), diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberView.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberView.kt index 240a3143af..b60fd285af 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberView.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/number/EnterNumberView.kt @@ -117,7 +117,7 @@ fun EnterNumberView( @PreviewsDayNight @Composable internal fun EnterNumberViewPreview( - @PreviewParameter(EnterNumberStateProvider::class) state: EnterNumberState, + @PreviewParameter(EnterNumberStatePreviewParam::class) state: EnterNumberState, ) = ElementPreview { EnterNumberView( state = state, diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeStateProvider.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeStatePreviewParam.kt similarity index 90% rename from features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeStateProvider.kt rename to features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeStatePreviewParam.kt index e6d33c2544..bc424746ae 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeStateProvider.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeStatePreviewParam.kt @@ -10,7 +10,7 @@ package io.element.android.features.linknewdevice.impl.screens.qrcode import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncData -class ShowQrCodeStateProvider : PreviewParameterProvider { +class ShowQrCodeStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aShowQrCodeState(), diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeView.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeView.kt index f2cd07f4a5..4cd22e37d2 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeView.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/qrcode/ShowQrCodeView.kt @@ -117,7 +117,7 @@ private fun QrCodeOrLoading( @PreviewsDayNight @Composable internal fun ShowQrCodeViewPreview( - @PreviewParameter(ShowQrCodeStateProvider::class) state: ShowQrCodeState, + @PreviewParameter(ShowQrCodeStatePreviewParam::class) state: ShowQrCodeState, ) = ElementPreview { ShowQrCodeView( state = state, diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootStateProvider.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootStatePreviewParam.kt similarity index 94% rename from features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootStateProvider.kt rename to features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootStatePreviewParam.kt index f1bb7ad455..20721e4a85 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootStateProvider.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootStatePreviewParam.kt @@ -11,7 +11,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.linknewdevice.ErrorType -open class LinkNewDeviceRootStateProvider : PreviewParameterProvider { +open class LinkNewDeviceRootStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aLinkNewDeviceRootState(), diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootView.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootView.kt index 5778abfb2c..b27cce17d9 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootView.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/root/LinkNewDeviceRootView.kt @@ -140,7 +140,7 @@ fun LinkNewDeviceRootView( @PreviewsDayNight @Composable internal fun LinkNewDeviceRootViewPreview( - @PreviewParameter(LinkNewDeviceRootStateProvider::class) state: LinkNewDeviceRootState + @PreviewParameter(LinkNewDeviceRootStatePreviewParam::class) state: LinkNewDeviceRootState ) = ElementPreview { LinkNewDeviceRootView( state = state, diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/scan/ScanQrCodeStateProvider.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/scan/ScanQrCodeStatePreviewParam.kt similarity index 91% rename from features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/scan/ScanQrCodeStateProvider.kt rename to features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/scan/ScanQrCodeStatePreviewParam.kt index 40e7df8884..a088bf7bd3 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/scan/ScanQrCodeStateProvider.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/scan/ScanQrCodeStatePreviewParam.kt @@ -10,7 +10,7 @@ package io.element.android.features.linknewdevice.impl.screens.scan import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction -open class ScanQrCodeStateProvider : PreviewParameterProvider { +open class ScanQrCodeStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aScanQrCodeState(), diff --git a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/scan/ScanQrCodeView.kt b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/scan/ScanQrCodeView.kt index 937aee08b4..4cd30fd904 100644 --- a/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/scan/ScanQrCodeView.kt +++ b/features/linknewdevice/impl/src/main/kotlin/io/element/android/features/linknewdevice/impl/screens/scan/ScanQrCodeView.kt @@ -166,7 +166,7 @@ private fun ColumnScope.Buttons( @PreviewsDayNight @Composable -internal fun ScanQrCodeViewPreview(@PreviewParameter(ScanQrCodeStateProvider::class) state: ScanQrCodeState) = ElementPreview { +internal fun ScanQrCodeViewPreview(@PreviewParameter(ScanQrCodeStatePreviewParam::class) state: ScanQrCodeState) = ElementPreview { ScanQrCodeView( state = state, onBackClick = {}, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/DefaultPermissionsPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/DefaultPermissionsPresenter.kt index 5aa7ae6f63..01fa8bca7e 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/DefaultPermissionsPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/DefaultPermissionsPresenter.kt @@ -41,9 +41,9 @@ class DefaultPermissionsPresenter( permissionsRequested = true } - fun handleEvent(event: PermissionsEvents) { + fun handleEvent(event: PermissionsEvent) { when (event) { - PermissionsEvents.RequestPermissions -> { + PermissionsEvent.RequestPermissions -> { multiplePermissionsState.launchMultiplePermissionRequest() } } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsEvents.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsEvent.kt similarity index 77% rename from features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsEvents.kt rename to features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsEvent.kt index a4b5ef1228..226909af5d 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsEvents.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsEvent.kt @@ -8,6 +8,6 @@ package io.element.android.features.location.impl.common.permissions -sealed interface PermissionsEvents { - data object RequestPermissions : PermissionsEvents +sealed interface PermissionsEvent { + data object RequestPermissions : PermissionsEvent } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt index 25ceb3ba29..a8dfd571e5 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/permissions/PermissionsState.kt @@ -12,7 +12,7 @@ data class PermissionsState( val permissions: Permissions, val shouldShowRationale: Boolean, val permissionsAlreadyRequested: Boolean, - val eventSink: (PermissionsEvents) -> Unit, + val eventSink: (PermissionsEvent) -> Unit, ) { sealed interface Permissions { data object AllGranted : Permissions diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt index 55f1e088d2..ccdf598e93 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt @@ -29,7 +29,7 @@ import io.element.android.features.location.impl.common.MapDefaults import io.element.android.features.location.impl.common.SendLiveLocationPermissions import io.element.android.features.location.impl.common.actions.LocationActions import io.element.android.features.location.impl.common.checkLocationConstraints -import io.element.android.features.location.impl.common.permissions.PermissionsEvents +import io.element.android.features.location.impl.common.permissions.PermissionsEvent import io.element.android.features.location.impl.common.permissions.PermissionsPresenter import io.element.android.features.location.impl.common.permissions.PermissionsState import io.element.android.features.location.impl.common.sendLiveLocationPermissions @@ -111,7 +111,7 @@ class ShareLocationPresenter( sendLiveLocationPermissions = SendLiveLocationPermissions.GRANTED ) if (locationConstraints is LocationConstraintsCheck.PermissionShouldBeRequested) { - permissionsState.eventSink(PermissionsEvents.RequestPermissions) + permissionsState.eventSink(PermissionsEvent.RequestPermissions) } trackUserPosition = locationConstraints is LocationConstraintsCheck.Success dialogState = ShareLocationState.Dialog.Constraints(locationConstraints.toDialogState()) @@ -137,7 +137,7 @@ class ShareLocationPresenter( } else -> { if (locationConstraints is LocationConstraintsCheck.PermissionShouldBeRequested) { - permissionsState.eventSink(PermissionsEvents.RequestPermissions) + permissionsState.eventSink(PermissionsEvent.RequestPermissions) } dialogState = ShareLocationState.Dialog.Constraints(locationConstraints.toDialogState()) } @@ -195,7 +195,7 @@ class ShareLocationPresenter( } ShareLocationEvent.RequestPermissions -> { dialogState = ShareLocationState.Dialog.None - permissionsState.eventSink(PermissionsEvents.RequestPermissions) + permissionsState.eventSink(PermissionsEvent.RequestPermissions) } } } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStatePreviewParam.kt similarity index 98% rename from features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt rename to features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStatePreviewParam.kt index 9214d5a017..908c7f0641 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStatePreviewParam.kt @@ -22,7 +22,7 @@ import kotlin.time.Duration.Companion.minutes private const val APP_NAME = "ApplicationName" -class ShareLocationStateProvider : PreviewParameterProvider { +class ShareLocationStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aShareLocationState( diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt index 53e7874936..36827cc919 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt @@ -332,7 +332,7 @@ private fun LiveLocationDurationDialog( @PreviewsDayNight @Composable internal fun ShareLocationViewPreview( - @PreviewParameter(ShareLocationStateProvider::class) state: ShareLocationState + @PreviewParameter(ShareLocationStatePreviewParam::class) state: ShareLocationState ) = ElementPreview { ShareLocationView( state = state, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt index bbec7b449f..412e96eb9c 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt @@ -32,7 +32,7 @@ import io.element.android.features.location.impl.common.MapDefaults import io.element.android.features.location.impl.common.SendLiveLocationPermissions import io.element.android.features.location.impl.common.actions.LocationActions import io.element.android.features.location.impl.common.checkLocationConstraints -import io.element.android.features.location.impl.common.permissions.PermissionsEvents +import io.element.android.features.location.impl.common.permissions.PermissionsEvent import io.element.android.features.location.impl.common.permissions.PermissionsPresenter import io.element.android.features.location.impl.common.permissions.PermissionsState import io.element.android.features.location.impl.common.toDialogState @@ -99,7 +99,7 @@ class ShowLocationPresenter( sendLiveLocationPermissions = SendLiveLocationPermissions.GRANTED ) if (locationConstraints is LocationConstraintsCheck.PermissionShouldBeRequested) { - permissionsState.eventSink(PermissionsEvents.RequestPermissions) + permissionsState.eventSink(PermissionsEvent.RequestPermissions) } isTrackMyLocation = locationConstraints is LocationConstraintsCheck.Success dialogState = locationConstraints.toDialogState() @@ -130,7 +130,7 @@ class ShowLocationPresenter( locationActions.openLocationSettings() dialogState = LocationConstraintsDialogState.None } - ShowLocationEvent.RequestPermissions -> permissionsState.eventSink(PermissionsEvents.RequestPermissions) + ShowLocationEvent.RequestPermissions -> permissionsState.eventSink(PermissionsEvent.RequestPermissions) ShowLocationEvent.StopLocationSharing -> coroutineScope.launch { liveLocationShareManager.stopShare(joinedRoom.roomId) } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStatePreviewParam.kt similarity index 97% rename from features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt rename to features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStatePreviewParam.kt index bf582eb59a..8473eaeb04 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStatePreviewParam.kt @@ -21,7 +21,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.location.AssetType import kotlinx.collections.immutable.toImmutableList -class ShowLocationStateProvider : PreviewParameterProvider { +class ShowLocationStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aShowLocationState(), diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt index 45e4c7f4cc..26eb1ee27a 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt @@ -200,7 +200,7 @@ fun ShowLocationView( @PreviewsDayNight @Composable -internal fun ShowLocationViewPreview(@PreviewParameter(ShowLocationStateProvider::class) state: ShowLocationState) = ElementPreview { +internal fun ShowLocationViewPreview(@PreviewParameter(ShowLocationStatePreviewParam::class) state: ShowLocationState) = ElementPreview { ShowLocationView( state = state, onBackClick = {}, diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/permissions/FakePermissionsPresenter.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/permissions/FakePermissionsPresenter.kt index 9b8ff7a29a..c25cbc24fc 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/permissions/FakePermissionsPresenter.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/permissions/FakePermissionsPresenter.kt @@ -11,9 +11,9 @@ package io.element.android.features.location.impl.common.permissions import androidx.compose.runtime.Composable class FakePermissionsPresenter : PermissionsPresenter { - val events = mutableListOf() + val events = mutableListOf() - private fun handleEvent(event: PermissionsEvents) { + private fun handleEvent(event: PermissionsEvent) { events += event } diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt index d249fdaf69..7500d29616 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt @@ -26,7 +26,7 @@ import io.element.android.features.location.impl.aPermissionsState import io.element.android.features.location.impl.common.FakeUserLocationStateFactory import io.element.android.features.location.impl.common.actions.FakeLocationActions import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter -import io.element.android.features.location.impl.common.permissions.PermissionsEvents +import io.element.android.features.location.impl.common.permissions.PermissionsEvent import io.element.android.features.location.impl.common.permissions.PermissionsState import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState import io.element.android.features.location.impl.live.LiveLocationStore @@ -174,7 +174,7 @@ class ShareLocationPresenterTest { shareLocationPresenter.test { skipItems(2) cancelAndIgnoreRemainingEvents() - assertThat(fakePermissionsPresenter.events).contains(PermissionsEvents.RequestPermissions) + assertThat(fakePermissionsPresenter.events).contains(PermissionsEvent.RequestPermissions) } } @@ -298,7 +298,7 @@ class ShareLocationPresenterTest { // Wait for dialog to be dismissed awaitItem() - assertThat(fakePermissionsPresenter.events.last()).isEqualTo(PermissionsEvents.RequestPermissions) + assertThat(fakePermissionsPresenter.events.last()).isEqualTo(PermissionsEvent.RequestPermissions) cancelAndIgnoreRemainingEvents() } } diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt index 7d9b0d5e03..1fab0d98af 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt @@ -20,7 +20,7 @@ import io.element.android.features.location.impl.aPermissionsState import io.element.android.features.location.impl.common.FakeUserLocationStateFactory import io.element.android.features.location.impl.common.actions.FakeLocationActions import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter -import io.element.android.features.location.impl.common.permissions.PermissionsEvents +import io.element.android.features.location.impl.common.permissions.PermissionsEvent import io.element.android.features.location.impl.common.permissions.PermissionsState import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState import io.element.android.features.location.test.FakeActiveLiveLocationShareManager @@ -188,7 +188,7 @@ class ShowLocationPresenterTest { // Continue the dialog sends permission request to the permissions presenter trackLocationState.eventSink(ShowLocationEvent.RequestPermissions) - assertThat(fakePermissionsPresenter.events.last()).isEqualTo(PermissionsEvents.RequestPermissions) + assertThat(fakePermissionsPresenter.events.last()).isEqualTo(PermissionsEvent.RequestPermissions) } } @@ -208,7 +208,7 @@ class ShowLocationPresenterTest { initialState.eventSink(ShowLocationEvent.TrackMyLocation(true)) - assertThat(fakePermissionsPresenter.events).contains(PermissionsEvents.RequestPermissions) + assertThat(fakePermissionsPresenter.events).contains(PermissionsEvent.RequestPermissions) cancelAndIgnoreRemainingEvents() } } diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenConfig.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenConfig.kt index c246390a63..fa3ca9cfd9 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenConfig.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/LockScreenConfig.kt @@ -8,12 +8,7 @@ package io.element.android.features.lockscreen.impl -import dev.zacsweers.metro.AppScope -import dev.zacsweers.metro.BindingContainer -import dev.zacsweers.metro.ContributesTo -import dev.zacsweers.metro.Provides import kotlin.time.Duration -import io.element.android.appconfig.LockScreenConfig as AppConfigLockScreenConfig data class LockScreenConfig( val isPinMandatory: Boolean, @@ -24,18 +19,3 @@ data class LockScreenConfig( val isStrongBiometricsEnabled: Boolean, val isWeakBiometricsEnabled: Boolean, ) - -@ContributesTo(AppScope::class) -@BindingContainer -object LockScreenConfigModule { - @Provides - fun providesLockScreenConfig(): LockScreenConfig = LockScreenConfig( - isPinMandatory = AppConfigLockScreenConfig.IS_PIN_MANDATORY, - forbiddenPinCodes = AppConfigLockScreenConfig.FORBIDDEN_PIN_CODES, - pinSize = AppConfigLockScreenConfig.PIN_SIZE, - maxPinCodeAttemptsBeforeLogout = AppConfigLockScreenConfig.MAX_PIN_CODE_ATTEMPTS_BEFORE_LOGOUT, - gracePeriod = AppConfigLockScreenConfig.GRACE_PERIOD, - isStrongBiometricsEnabled = AppConfigLockScreenConfig.IS_STRONG_BIOMETRICS_ENABLED, - isWeakBiometricsEnabled = AppConfigLockScreenConfig.IS_WEAK_BIOMETRICS_ENABLED, - ) -} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/di/LockScreenConfigBindingContainer.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/di/LockScreenConfigBindingContainer.kt new file mode 100644 index 0000000000..3acade5e86 --- /dev/null +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/di/LockScreenConfigBindingContainer.kt @@ -0,0 +1,29 @@ +/* + * 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.lockscreen.impl.di + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides +import io.element.android.features.lockscreen.impl.LockScreenConfig + +@ContributesTo(AppScope::class) +@BindingContainer +object LockScreenConfigBindingContainer { + @Provides + fun providesLockScreenConfig(): LockScreenConfig = LockScreenConfig( + isPinMandatory = io.element.android.appconfig.LockScreenConfig.IS_PIN_MANDATORY, + forbiddenPinCodes = io.element.android.appconfig.LockScreenConfig.FORBIDDEN_PIN_CODES, + pinSize = io.element.android.appconfig.LockScreenConfig.PIN_SIZE, + maxPinCodeAttemptsBeforeLogout = io.element.android.appconfig.LockScreenConfig.MAX_PIN_CODE_ATTEMPTS_BEFORE_LOGOUT, + gracePeriod = io.element.android.appconfig.LockScreenConfig.GRACE_PERIOD, + isStrongBiometricsEnabled = io.element.android.appconfig.LockScreenConfig.IS_STRONG_BIOMETRICS_ENABLED, + isWeakBiometricsEnabled = io.element.android.appconfig.LockScreenConfig.IS_WEAK_BIOMETRICS_ENABLED, + ) +} diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStatePreviewParam.kt similarity index 91% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStatePreviewParam.kt index 43f20c1c52..8929138077 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsStatePreviewParam.kt @@ -10,7 +10,7 @@ package io.element.android.features.lockscreen.impl.settings import androidx.compose.ui.tooling.preview.PreviewParameterProvider -open class LockScreenSettingsStateProvider : PreviewParameterProvider { +open class LockScreenSettingsStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aLockScreenSettingsState(), diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt index d8655e29a3..291906caf0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsView.kt @@ -84,7 +84,7 @@ fun LockScreenSettingsView( @PreviewsDayNight @Composable internal fun LockScreenSettingsViewPreview( - @PreviewParameter(LockScreenSettingsStateProvider::class) state: LockScreenSettingsState, + @PreviewParameter(LockScreenSettingsStatePreviewParam::class) state: LockScreenSettingsState, ) { ElementPreview { LockScreenSettingsView( diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricStatePreviewParam.kt similarity index 87% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricStateProvider.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricStatePreviewParam.kt index d725d2de97..6ad44f3273 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricStatePreviewParam.kt @@ -10,7 +10,7 @@ package io.element.android.features.lockscreen.impl.setup.biometric import androidx.compose.ui.tooling.preview.PreviewParameterProvider -open class SetupBiometricStateProvider : PreviewParameterProvider { +open class SetupBiometricStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aSetupBiometricState(), diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt index 70a1046e36..13fad4e9d0 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/biometric/SetupBiometricView.kt @@ -79,7 +79,7 @@ private fun SetupBiometricFooter( @Composable @PreviewsDayNight -internal fun SetupBiometricViewPreview(@PreviewParameter(SetupBiometricStateProvider::class) state: SetupBiometricState) { +internal fun SetupBiometricViewPreview(@PreviewParameter(SetupBiometricStatePreviewParam::class) state: SetupBiometricState) { ElementPreview { SetupBiometricView( state = state, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinStatePreviewParam.kt similarity index 95% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinStateProvider.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinStatePreviewParam.kt index f50643e367..706d49ce82 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinStatePreviewParam.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.features.lockscreen.impl.setup.pin.validation.SetupPinFailure -open class SetupPinStateProvider : PreviewParameterProvider { +open class SetupPinStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aSetupPinState(), diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt index 508d3c1fbb..7b0f86e386 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/setup/pin/SetupPinView.kt @@ -145,7 +145,7 @@ private fun SetupPinFailure.title(): String { @Composable @PreviewsDayNight -internal fun SetupPinViewPreview(@PreviewParameter(SetupPinStateProvider::class) state: SetupPinState) { +internal fun SetupPinViewPreview(@PreviewParameter(SetupPinStatePreviewParam::class) state: SetupPinState) { ElementPreview { SetupPinView( state = state, diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStatePreviewParam.kt similarity index 94% rename from features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt rename to features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStatePreviewParam.kt index aef2c057a2..dcc61e83ed 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStateProvider.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockStatePreviewParam.kt @@ -16,7 +16,7 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData -open class PinUnlockStateProvider : PreviewParameterProvider { +open class PinUnlockStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aPinUnlockState(), @@ -42,7 +42,7 @@ open class PinUnlockStateProvider : PreviewParameterProvider { ) } -open class PinUnlockStateCompactProvider : PreviewParameterProvider { +open class PinUnlockStateCompactPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aPinUnlockState(), diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt index 0292b29a0f..e83b106d88 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/PinUnlockView.kt @@ -381,7 +381,7 @@ private fun PinUnlockFooter( @Composable @PreviewsDayNight -internal fun PinUnlockViewInAppPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) { +internal fun PinUnlockViewInAppPreview(@PreviewParameter(PinUnlockStatePreviewParam::class) state: PinUnlockState) { ElementPreview { PinUnlockView( state = state, @@ -393,7 +393,7 @@ internal fun PinUnlockViewInAppPreview(@PreviewParameter(PinUnlockStateProvider: @Composable @PreviewsDayNight -internal fun PinUnlockViewPreview(@PreviewParameter(PinUnlockStateProvider::class) state: PinUnlockState) { +internal fun PinUnlockViewPreview(@PreviewParameter(PinUnlockStatePreviewParam::class) state: PinUnlockState) { ElementPreview { PinUnlockView( state = state, @@ -405,7 +405,7 @@ internal fun PinUnlockViewPreview(@PreviewParameter(PinUnlockStateProvider::clas @Composable @Preview(heightDp = 480, widthDp = 800) -internal fun PinUnlockViewCompactPreview(@PreviewParameter(PinUnlockStateCompactProvider::class) state: PinUnlockState) { +internal fun PinUnlockViewCompactPreview(@PreviewParameter(PinUnlockStateCompactPreviewParam::class) state: PinUnlockState) { ElementPreview { PinUnlockView( state = state, diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index 86a8e0e7bc..6cf8945e05 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -69,6 +69,7 @@ dependencies { implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.qrcode) implementation(projects.libraries.oauth.api) + implementation(projects.libraries.preferences.api) implementation(projects.libraries.uiUtils) implementation(projects.libraries.wellknown.api) implementation(libs.androidx.browser) @@ -80,6 +81,7 @@ dependencies { testImplementation(projects.features.login.test) testImplementation(projects.features.enterprise.test) testImplementation(projects.features.preferences.test) + testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.oauth.test) testImplementation(projects.libraries.permissions.test) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt index a662f20351..bbfe7c47d8 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt @@ -23,7 +23,6 @@ import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.replace -import com.bumble.appyx.navmodel.backstack.operation.singleTop import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject @@ -33,13 +32,11 @@ import io.element.android.features.login.api.LoginEntryPoint import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.classic.ElementClassicConnection import io.element.android.features.login.impl.qrcode.QrCodeLoginFlowNode -import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderNode import io.element.android.features.login.impl.screens.classic.ClassicFlowNode import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode import io.element.android.features.login.impl.screens.onboarding.OnBoardingNode -import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.architecture.BackstackView @@ -129,12 +126,6 @@ class LoginFlowNode( @Parcelize data object ChooseAccountProvider : NavTarget - @Parcelize - data object ChangeAccountProvider : NavTarget - - @Parcelize - data object SearchAccountProvider : NavTarget - @Parcelize data class LoginPassword( val initialLogin: String = "", @@ -261,43 +252,9 @@ class LoginFlowNode( override fun navigateToLoginPassword() { backstack.push(NavTarget.LoginPassword()) } - - override fun navigateToChangeAccountProvider() { - backstack.push(NavTarget.ChangeAccountProvider) - } } createNode(buildContext, plugins = listOf(inputs, callback)) } - NavTarget.ChangeAccountProvider -> { - val callback = object : ChangeAccountProviderNode.Callback { - override fun onDone() { - // Go back to the Account Provider screen - val confirmAccountProvider = backstack.elements.value.firstOrNull { - it.key.navTarget is NavTarget.ConfirmAccountProvider - }?.key?.navTarget ?: NavTarget.ConfirmAccountProvider(isAccountCreation = false) - backstack.singleTop(confirmAccountProvider) - } - - override fun navigateToSearchAccountProvider() { - backstack.push(NavTarget.SearchAccountProvider) - } - } - - createNode(buildContext, plugins = listOf(callback)) - } - NavTarget.SearchAccountProvider -> { - val callback = object : SearchAccountProviderNode.Callback { - override fun onDone() { - // Go back to the Account Provider screen - val confirmAccountProvider = backstack.elements.value.firstOrNull { - it.key.navTarget is NavTarget.ConfirmAccountProvider - }?.key?.navTarget ?: NavTarget.ConfirmAccountProvider(isAccountCreation = false) - backstack.singleTop(confirmAccountProvider) - } - } - - createNode(buildContext, plugins = listOf(callback)) - } is NavTarget.LoginPassword -> { val inputs = LoginPasswordNode.Inputs( initialLogin = navTarget.initialLogin, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt index ad016dec26..6d85f2c5ff 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt @@ -13,27 +13,64 @@ import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.features.enterprise.api.canConnectToAnyHomeserver +import io.element.android.libraries.di.annotations.AppCoroutineScope +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch @SingleIn(AppScope::class) @Inject class AccountProviderDataSource( - enterpriseService: EnterpriseService, + private val enterpriseService: EnterpriseService, + private val appPreferencesStore: AppPreferencesStore, + @AppCoroutineScope private val coroutineScope: CoroutineScope, ) { - private val defaultAccountProvider = createAccountProvider( + // The provider used when the user has not selected one: an enterprise/MDM-configured provider, + // else matrix.org. The most recently used provider (from history) can override it, see init. + private val configuredAccountProvider = createAccountProvider( url = enterpriseService.homeserverAllowList() .firstOrNull { it != EnterpriseService.ANY_ACCOUNT_PROVIDER } ?: AuthenticationConfig.MATRIX_ORG_URL ) - private val accountProvider: MutableStateFlow = MutableStateFlow(defaultAccountProvider) + private val accountProvider: MutableStateFlow = MutableStateFlow(configuredAccountProvider) val flow: StateFlow = accountProvider.asStateFlow() + // The account provider the user last explicitly selected (via [setAccountProvider] / [setUrl]). + // Unlike [flow], this is not recomputed by [reset], so it survives to be persisted to history on a + // successful sign-in even when the login flow is torn down in between (e.g. across an OAuth round-trip). + var lastSelectedAccountProviderUrl: String? = null + private set + + init { + // Seed the default from the last used provider, unless the user has already selected one. + coroutineScope.launch { + val default = defaultAccountProvider() + accountProvider.update { current -> if (current == configuredAccountProvider) default else current } + } + } + suspend fun reset() { - accountProvider.emit(defaultAccountProvider) + accountProvider.emit(defaultAccountProvider()) + } + + /** + * The provider to default to: the most recently used one from history when the user is free to + * connect to any provider, otherwise the enterprise/MDM-configured provider. + */ + private suspend fun defaultAccountProvider(): AccountProvider { + if (!enterpriseService.canConnectToAnyHomeserver()) { + return configuredAccountProvider + } + val lastUsedProvider = appPreferencesStore.getHomeserverHistoryFlow().first().firstOrNull() + return lastUsedProvider?.let { createAccountProvider(it) } ?: configuredAccountProvider } suspend fun setUrl(url: String) { @@ -41,6 +78,7 @@ class AccountProviderDataSource( } suspend fun setAccountProvider(data: AccountProvider) { + lastSelectedAccountProviderUrl = data.url accountProvider.emit(data) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderOtherView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderOtherView.kt deleted file mode 100644 index 8b9b22022e..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderOtherView.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.login.impl.accountprovider - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.unit.dp -import io.element.android.compound.theme.ElementTheme -import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.login.impl.R -import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom -import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.HorizontalDivider -import io.element.android.libraries.designsystem.theme.components.Text - -/** - * https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817 - */ -@Composable -fun AccountProviderOtherView( - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier - .fillMaxWidth() - .clickable(role = Role.Button) { onClick() } - ) { - HorizontalDivider() - Row( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 44.dp) - .padding(vertical = 4.dp, horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RoundedIconAtom( - size = RoundedIconAtomSize.Medium, - imageVector = CompoundIcons.Search(), - tint = ElementTheme.colors.iconPrimary, - ) - Text( - modifier = Modifier - .padding(start = 16.dp) - .weight(1f), - text = stringResource(R.string.screen_change_account_provider_other), - style = ElementTheme.typography.fontBodyLgMedium, - color = ElementTheme.colors.textPrimary, - ) - } - } -} - -@PreviewsDayNight -@Composable -internal fun AccountProviderOtherViewPreview() = ElementPreview { - AccountProviderOtherView( - onClick = { }, - ) -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPreviewParam.kt similarity index 93% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPreviewParam.kt index 745939233f..c14827d1a6 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPreviewParam.kt @@ -11,7 +11,7 @@ package io.element.android.features.login.impl.accountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.appconfig.AuthenticationConfig -open class AccountProviderProvider : PreviewParameterProvider { +open class AccountProviderPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( anAccountProvider(), diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt index fcf4dac3b1..09b423f713 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt @@ -116,7 +116,7 @@ fun AccountProviderView( @PreviewsDayNight @Composable -internal fun AccountProviderViewPreview(@PreviewParameter(AccountProviderProvider::class) item: AccountProvider) = ElementPreview { +internal fun AccountProviderViewPreview(@PreviewParameter(AccountProviderPreviewParam::class) item: AccountProvider) = ElementPreview { AccountProviderView( item = item, onClick = { } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/SaveAccountProviderToHistory.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/SaveAccountProviderToHistory.kt new file mode 100644 index 0000000000..b3b99115cc --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/SaveAccountProviderToHistory.kt @@ -0,0 +1,32 @@ +/* + * 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.login.impl.accountprovider + +import dev.zacsweers.metro.Inject +import io.element.android.libraries.preferences.api.store.AppPreferencesStore + +/** + * Persist the account provider the user just authenticated against, so that it can be offered as + * the default (see [AccountProviderDataSource]) and as an autocomplete suggestion on the next + * sign-in. To be called from the manual sign-in / account creation flows only, on success. + * + * See [AppPreferencesStore.addHomeserverToHistory]. + */ +@Inject +class SaveAccountProviderToHistory( + private val accountProviderDataSource: AccountProviderDataSource, + private val appPreferencesStore: AppPreferencesStore, +) { + suspend operator fun invoke() { + // Persist the provider the user actually selected, not the currently-exposed default: the latter can be + // reset back to the history default while the login flow is torn down across an OAuth round-trip. + val url = accountProviderDataSource.lastSelectedAccountProviderUrl + ?: accountProviderDataSource.flow.value.url + appPreferencesStore.addHomeserverToHistory(url) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvent.kt similarity index 83% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvent.kt index 4f0d228dd2..f3fd02713f 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvent.kt @@ -10,9 +10,9 @@ package io.element.android.features.login.impl.changeserver import io.element.android.features.login.impl.accountprovider.AccountProvider -sealed interface ChangeServerEvents { - data class ChangeServer(val accountProvider: AccountProvider) : ChangeServerEvents - data object ClearError : ChangeServerEvents - data object RequestLocalNetworkPermission : ChangeServerEvents - data object DismissLocalNetworkPermission : ChangeServerEvents +sealed interface ChangeServerEvent { + data class ChangeServer(val accountProvider: AccountProvider) : ChangeServerEvent + data object ClearError : ChangeServerEvent + data object RequestLocalNetworkPermission : ChangeServerEvent + data object DismissLocalNetworkPermission : ChangeServerEvent } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt index 79f76d5fb5..9106d6e1c4 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails @Inject class ChangeServerPresenter( @@ -32,7 +33,7 @@ class ChangeServerPresenter( ) : Presenter { @Composable override fun present(): ChangeServerState { - val changeServerAction: MutableState> = remember { + val changeServerAction: MutableState> = remember { mutableStateOf(AsyncData.Uninitialized) } @@ -41,12 +42,12 @@ class ChangeServerPresenter( onProceed = { provider -> changeServer(provider, changeServerAction) }, ) - fun handleEvent(event: ChangeServerEvents) { + fun handleEvent(event: ChangeServerEvent) { when (event) { - ChangeServerEvents.ClearError -> changeServerAction.value = AsyncData.Uninitialized - is ChangeServerEvents.ChangeServer -> gateState.submit(event.accountProvider) - ChangeServerEvents.DismissLocalNetworkPermission -> gateState.abort() - ChangeServerEvents.RequestLocalNetworkPermission -> gateState.requestPermission() + ChangeServerEvent.ClearError -> changeServerAction.value = AsyncData.Uninitialized + is ChangeServerEvent.ChangeServer -> gateState.submit(event.accountProvider) + ChangeServerEvent.DismissLocalNetworkPermission -> gateState.abort() + ChangeServerEvent.RequestLocalNetworkPermission -> gateState.requestPermission() } } @@ -59,7 +60,7 @@ class ChangeServerPresenter( private suspend fun changeServer( data: AccountProvider, - changeServerAction: MutableState>, + changeServerAction: MutableState>, ) { suspend { defaultAccountProviderAccessControl.assertIsAllowedToConnectToAccountProvider( @@ -72,6 +73,8 @@ class ChangeServerPresenter( } // Homeserver is valid, remember user choice accountProviderDataSource.setAccountProvider(data) + // Return the resolved details so the caller can sign in without configuring the homeserver again. + details }.runCatchingUpdatingState(changeServerAction, errorTransform = ChangeServerError::from) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt index 6d8ce25d75..1b69551e94 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt @@ -9,10 +9,13 @@ package io.element.android.features.login.impl.changeserver import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog data class ChangeServerState( - val changeServerAction: AsyncData, + // On success, carries the resolved homeserver details so the caller can proceed with login without + // configuring (and re-networking) the homeserver a second time. + val changeServerAction: AsyncData, val localNetworkPermissionDialog: LocalNetworkPermissionDialog, - val eventSink: (ChangeServerEvents) -> Unit + val eventSink: (ChangeServerEvent) -> Unit ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStatePreviewParam.kt similarity index 89% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStatePreviewParam.kt index 183158bceb..2654953ece 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStatePreviewParam.kt @@ -11,9 +11,10 @@ package io.element.android.features.login.impl.changeserver import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog -open class ChangeServerStateProvider : PreviewParameterProvider { +open class ChangeServerStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aChangeServerState(), @@ -45,7 +46,7 @@ open class ChangeServerStateProvider : PreviewParameterProvider = AsyncData.Uninitialized, + changeServerAction: AsyncData = AsyncData.Uninitialized, localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None, ) = ChangeServerState( changeServerAction = changeServerAction, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt index 30ae644a70..e629c6bce3 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt @@ -47,7 +47,7 @@ fun ChangeServerView( modifier = modifier, content = stringResource(R.string.screen_change_server_error_invalid_homeserver), onSubmit = { - eventSink.invoke(ChangeServerEvents.ClearError) + eventSink.invoke(ChangeServerEvent.ClearError) } ) ChangeServerError.UnsupportedServer -> @@ -55,7 +55,7 @@ fun ChangeServerView( modifier = modifier, content = stringResource(R.string.screen_login_error_unsupported_authentication), onSubmit = { - eventSink.invoke(ChangeServerEvents.ClearError) + eventSink.invoke(ChangeServerEvent.ClearError) } ) is ChangeServerError.Error -> { @@ -63,7 +63,7 @@ fun ChangeServerView( modifier = modifier, content = error.messageStr ?: stringResource(CommonStrings.error_unknown), onSubmit = { - eventSink.invoke(ChangeServerEvents.ClearError) + eventSink.invoke(ChangeServerEvent.ClearError) } ) } @@ -72,10 +72,10 @@ fun ChangeServerView( modifier = modifier, onLearnMoreClick = { onLearnMoreClick() - eventSink.invoke(ChangeServerEvents.ClearError) + eventSink.invoke(ChangeServerEvent.ClearError) }, onDismiss = { - eventSink.invoke(ChangeServerEvents.ClearError) + eventSink.invoke(ChangeServerEvent.ClearError) } ) } @@ -90,10 +90,10 @@ fun ChangeServerView( submitText = stringResource(R.string.screen_change_server_error_element_pro_required_action_android), onSubmitClick = { context.openGooglePlay(error.applicationId) - eventSink.invoke(ChangeServerEvents.ClearError) + eventSink.invoke(ChangeServerEvent.ClearError) }, onDismiss = { - eventSink.invoke(ChangeServerEvents.ClearError) + eventSink.invoke(ChangeServerEvent.ClearError) }, ) } @@ -106,7 +106,7 @@ fun ChangeServerView( error.unauthorisedAccountProviderTitle, ), onSubmit = { - eventSink.invoke(ChangeServerEvents.ClearError) + eventSink.invoke(ChangeServerEvent.ClearError) } ) } @@ -125,17 +125,17 @@ fun ChangeServerView( LocalNetworkPermissionDialogView( dialog = state.localNetworkPermissionDialog, onSubmit = { - eventSink.invoke(ChangeServerEvents.RequestLocalNetworkPermission) + eventSink.invoke(ChangeServerEvent.RequestLocalNetworkPermission) }, onDismiss = { - eventSink.invoke(ChangeServerEvents.DismissLocalNetworkPermission) + eventSink.invoke(ChangeServerEvent.DismissLocalNetworkPermission) } ) } @PreviewsDayNight @Composable -internal fun ChangeServerViewPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) = ElementPreview { +internal fun ChangeServerViewPreview(@PreviewParameter(ChangeServerStatePreviewParam::class) state: ChangeServerState) = ElementPreview { ChangeServerView( state = state, onLearnMoreClick = {}, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginBindingContainer.kt similarity index 96% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginBindingContainer.kt index 037acf859b..257587770f 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginBindingContainer.kt @@ -20,7 +20,7 @@ import io.element.android.libraries.architecture.Presenter @ContributesTo(AppScope::class) @BindingContainer -interface LoginModule { +interface LoginBindingContainer { @Binds fun bindChangeServerPresenter(presenter: ChangeServerPresenter): Presenter diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerErrorProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerErrorPreviewParam.kt similarity index 92% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerErrorProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerErrorPreviewParam.kt index 59fb42fe0c..76c8b18409 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerErrorProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerErrorPreviewParam.kt @@ -10,7 +10,7 @@ package io.element.android.features.login.impl.error import androidx.compose.ui.tooling.preview.PreviewParameterProvider -class ChangeServerErrorProvider : PreviewParameterProvider { +class ChangeServerErrorPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( ChangeServerError.InvalidServer, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeEvent.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeEvent.kt index 73400890d5..945b0bc580 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeEvent.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeEvent.kt @@ -7,12 +7,18 @@ package io.element.android.features.login.impl.login +import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails + sealed interface LoginModeEvent { data class Submit( val isAccountCreation: Boolean, val homeserverUrl: String, val resolvedHomeserverUrl: String?, val loginHint: String?, + // When the caller has already configured the homeserver (e.g. the account provider screen validated + // it first), these are its resolved details. When set, login reuses them instead of calling + // setHomeserver again, avoiding a redundant network round-trip. + val preConfiguredDetails: MatrixHomeServerDetails? = null, ) : LoginModeEvent data object ClearError : LoginModeEvent diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt index 466cd210ab..59d156308f 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import dev.zacsweers.metro.Inject +import io.element.android.features.login.impl.accountprovider.SaveAccountProviderToHistory import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderPresenter @@ -39,6 +40,7 @@ class LoginModePresenter( private val oAuthActionFlow: OAuthActionFlow, private val authenticationService: MatrixAuthenticationService, private val localNetworkPermissionGate: LocalNetworkPermissionGate, + private val saveAccountProviderToHistory: SaveAccountProviderToHistory, ) : Presenter { @Composable override fun present(): LoginModeState { @@ -73,26 +75,28 @@ class LoginModePresenter( private suspend fun performSubmit(request: LoginModeEvent.Submit, loginMode: MutableState>) { suspend { - authenticationService.setHomeserver(request.homeserverUrl).recoverCatching { - // Fallback to the well-known-resolved URL if the primary URL failed and the caller supplied one. - if (request.resolvedHomeserverUrl != null && request.resolvedHomeserverUrl != request.homeserverUrl) { - authenticationService.setHomeserver(request.resolvedHomeserverUrl).getOrThrow() - } else { - throw it - } - }.map { matrixHomeServerDetails -> - when { - matrixHomeServerDetails.supportsOAuthLogin -> { - val oAuthPrompt = if (request.isAccountCreation) OAuthPrompt.Create else OAuthPrompt.Login - LoginMode.OAuth( - authenticationService.getOAuthUrl(prompt = oAuthPrompt, loginHint = request.loginHint).getOrThrow() - ) + // Reuse the details the caller already resolved when it configured the homeserver, so we don't + // run setHomeserver (a network round-trip) again. Otherwise configure the homeserver ourselves. + val matrixHomeServerDetails = request.preConfiguredDetails + ?: authenticationService.setHomeserver(request.homeserverUrl).recoverCatching { + // Fallback to the well-known-resolved URL if the primary URL failed and the caller supplied one. + if (request.resolvedHomeserverUrl != null && request.resolvedHomeserverUrl != request.homeserverUrl) { + authenticationService.setHomeserver(request.resolvedHomeserverUrl).getOrThrow() + } else { + throw it } - request.isAccountCreation -> throw AccountCreationNotSupported() - matrixHomeServerDetails.supportsPasswordLogin -> LoginMode.PasswordLogin - else -> error("Unsupported authentication flow") + }.getOrThrow() + when { + matrixHomeServerDetails.supportsOAuthLogin -> { + val oAuthPrompt = if (request.isAccountCreation) OAuthPrompt.Create else OAuthPrompt.Login + LoginMode.OAuth( + authenticationService.getOAuthUrl(prompt = oAuthPrompt, loginHint = request.loginHint).getOrThrow() + ) } - }.getOrThrow() + request.isAccountCreation -> throw AccountCreationNotSupported() + matrixHomeServerDetails.supportsPasswordLogin -> LoginMode.PasswordLogin + else -> error("Unsupported authentication flow") + } }.runCatchingUpdatingState( state = loginMode, errorTransform = { @@ -116,6 +120,7 @@ class LoginModePresenter( .onSuccess { loginMode.value = AsyncData.Uninitialized } .onFailure { loginMode.value = AsyncData.Failure(it) } is OAuthAction.Success -> authenticationService.loginWithOAuth(action.url) + .onSuccess { saveAccountProviderToHistory() } .onFailure { loginMode.value = AsyncData.Failure(it) } } oAuthActionFlow.reset() diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt index 8b59db1d61..80784d2b13 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeView.kt @@ -129,7 +129,7 @@ fun LoginModeView( @PreviewsDayNight @Composable -internal fun LoginModeViewPreview(@PreviewParameter(LoginModeViewErrorProvider::class) error: Throwable) { +internal fun LoginModeViewPreview(@PreviewParameter(LoginModeViewErrorPreviewParam::class) error: Throwable) { ElementPreview { LoginModeView( loginMode = AsyncData.Failure(error), diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeViewErrorProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeViewErrorPreviewParam.kt similarity index 80% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeViewErrorProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeViewErrorPreviewParam.kt index 513d0a8a04..4706beefbb 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeViewErrorProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeViewErrorPreviewParam.kt @@ -9,11 +9,11 @@ package io.element.android.features.login.impl.login import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.error.ChangeServerErrorProvider +import io.element.android.features.login.impl.error.ChangeServerErrorPreviewParam import io.element.android.libraries.matrix.api.auth.AuthenticationException -class LoginModeViewErrorProvider : PreviewParameterProvider { +class LoginModeViewErrorPreviewParam : PreviewParameterProvider { override val values: Sequence - get() = ChangeServerErrorProvider().values + + get() = ChangeServerErrorPreviewParam().values + AuthenticationException.AccountAlreadyLoggedIn("@alice:matrix.org") } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt deleted file mode 100644 index d9994deb9a..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt +++ /dev/null @@ -1,14 +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.features.login.impl.resolver - -data class HomeserverData( - // The computed homeserver url, for which a wellknown file has been retrieved, or just a valid Url - val homeserverUrl: String, -) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt deleted file mode 100644 index 2700922678..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt +++ /dev/null @@ -1,89 +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.features.login.impl.resolver - -import dev.zacsweers.metro.Inject -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.core.coroutine.parallelMap -import io.element.android.libraries.core.uri.ensureProtocol -import io.element.android.libraries.core.uri.isValidUrl -import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker -import io.element.android.libraries.matrix.api.core.MatrixPatterns -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.withContext -import timber.log.Timber -import java.util.Collections - -/** - * Resolve homeserver base on search terms. - */ -@Inject -class HomeserverResolver( - private val dispatchers: CoroutineDispatchers, - private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker, - private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor, -) { - fun resolve(userInput: String): Flow> = flow { - val flowContext = currentCoroutineContext() - val trimmedUserInput = userInput.trim() - val searchTerm = when { - MatrixPatterns.isUserId(trimmedUserInput) -> UserId(trimmedUserInput).domainName.orEmpty() - trimmedUserInput.startsWith("@") -> return@flow - else -> trimmedUserInput - } - if (searchTerm.length < 4) return@flow - val candidateBase = searchTerm.ensureProtocol().removeSuffix("/") - val list = getUrlCandidates(candidateBase) - val currentList = Collections.synchronizedList(mutableListOf()) - // Run all the requests in parallel - withContext(dispatchers.io) { - list.parallelMap { url -> - // Skip the compatibility probe if we'd need ACCESS_LOCAL_NETWORK first — - // otherwise the probe hangs on the TCP timeout for ~30s. Emit the URL as a - // candidate directly; the actual sign-in flow triggers the permission prompt. - val shouldRequestPermissionOrIsValid = localNetworkPermissionAdvisor.shouldRequestPermissionFor(url) || - homeServerLoginCompatibilityChecker.check(url) - .onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") } - .getOrNull() - ?: return@parallelMap - - // Emit the list as soon as possible - if (shouldRequestPermissionOrIsValid) { - currentList.add(HomeserverData(homeserverUrl = url)) - withContext(flowContext) { - emit(currentList.toList()) - } - } - } - } - // If list is empty, and candidateBase is a valid an URL, do not block the user. - // A unsupported homeserver / homeserver not found error will be displayed if the user continues - if (currentList.isEmpty() && candidateBase.isValidUrl()) { - emit(listOf(HomeserverData(homeserverUrl = candidateBase))) - } - } - - private fun getUrlCandidates(data: String): List { - return buildList { - if (data.contains(".")) { - // TLD detected? - } else { - add("$data.org") - add("$data.com") - add("$data.io") - } - // Always try what the user has entered - add(data) - } - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderNode.kt deleted file mode 100644 index 018a25dc89..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderNode.kt +++ /dev/null @@ -1,51 +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.features.login.impl.screens.changeaccountprovider - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -import dev.zacsweers.metro.AppScope -import dev.zacsweers.metro.Assisted -import dev.zacsweers.metro.AssistedInject -import io.element.android.annotations.ContributesNode -import io.element.android.features.login.impl.util.openLearnMorePage -import io.element.android.libraries.architecture.callback - -@ContributesNode(AppScope::class) -@AssistedInject -class ChangeAccountProviderNode( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - private val presenter: ChangeAccountProviderPresenter, -) : Node(buildContext, plugins = plugins) { - interface Callback : Plugin { - fun onDone() - fun navigateToSearchAccountProvider() - } - - private val callback: Callback = callback() - - @Composable - override fun View(modifier: Modifier) { - val state = presenter.present() - val context = LocalContext.current - ChangeAccountProviderView( - state = state, - modifier = modifier, - onBackClick = ::navigateUp, - onLearnMoreClick = { openLearnMorePage(context) }, - onSuccess = callback::onDone, - onOtherProviderClick = callback::navigateToSearchAccountProvider, - ) - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt deleted file mode 100644 index 826cca0c2b..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt +++ /dev/null @@ -1,57 +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.features.login.impl.screens.changeaccountprovider - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import dev.zacsweers.metro.Inject -import io.element.android.appconfig.AuthenticationConfig -import io.element.android.features.enterprise.api.EnterpriseService -import io.element.android.features.enterprise.api.canConnectToAnyHomeserver -import io.element.android.features.login.impl.accountprovider.AccountProvider -import io.element.android.features.login.impl.changeserver.ChangeServerState -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.core.uri.ensureProtocol -import kotlinx.collections.immutable.toImmutableList - -@Inject -class ChangeAccountProviderPresenter( - private val changeServerPresenter: Presenter, - private val enterpriseService: EnterpriseService, -) : Presenter { - @Composable - override fun present(): ChangeAccountProviderState { - val staticAccountProviderList = remember { - enterpriseService.homeserverAllowList() - .filter { it != EnterpriseService.ANY_ACCOUNT_PROVIDER } - .map { it.ensureProtocol() } - .ifEmpty { listOf(AuthenticationConfig.MATRIX_ORG_URL) } - .map { url -> - AccountProvider( - url = url, - subtitle = null, - isPublic = url == AuthenticationConfig.MATRIX_ORG_URL, - isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL, - ) - } - .toImmutableList() - } - - val canSearchForAccountProviders = remember { - enterpriseService.canConnectToAnyHomeserver() - } - - val changeServerState = changeServerPresenter.present() - return ChangeAccountProviderState( - accountProviders = staticAccountProviderList, - canSearchForAccountProviders = canSearchForAccountProviders, - changeServerState = changeServerState, - ) - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt deleted file mode 100644 index 3068a1cb57..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt +++ /dev/null @@ -1,19 +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.features.login.impl.screens.changeaccountprovider - -import io.element.android.features.login.impl.accountprovider.AccountProvider -import io.element.android.features.login.impl.changeserver.ChangeServerState -import kotlinx.collections.immutable.ImmutableList - -data class ChangeAccountProviderState( - val accountProviders: ImmutableList, - val canSearchForAccountProviders: Boolean, - val changeServerState: ChangeServerState, -) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt deleted file mode 100644 index 5cec1961a4..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt +++ /dev/null @@ -1,37 +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.features.login.impl.screens.changeaccountprovider - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.accountprovider.AccountProvider -import io.element.android.features.login.impl.accountprovider.anAccountProvider -import io.element.android.features.login.impl.changeserver.ChangeServerState -import io.element.android.features.login.impl.changeserver.aChangeServerState -import kotlinx.collections.immutable.toImmutableList - -open class ChangeAccountProviderStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aChangeAccountProviderState(), - aChangeAccountProviderState(canSearchForAccountProviders = false), - // Add other state here - ) -} - -fun aChangeAccountProviderState( - accountProviders: List = listOf( - anAccountProvider() - ), - canSearchForAccountProviders: Boolean = true, - changeServerState: ChangeServerState = aChangeServerState(), -) = ChangeAccountProviderState( - accountProviders = accountProviders.toImmutableList(), - canSearchForAccountProviders = canSearchForAccountProviders, - changeServerState = changeServerState, -) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt deleted file mode 100644 index 212bab515d..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt +++ /dev/null @@ -1,129 +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. - */ - -@file:OptIn(ExperimentalMaterial3Api::class) - -package io.element.android.features.login.impl.screens.changeaccountprovider - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.login.impl.R -import io.element.android.features.login.impl.accountprovider.AccountProviderOtherView -import io.element.android.features.login.impl.accountprovider.AccountProviderView -import io.element.android.features.login.impl.changeserver.ChangeServerEvents -import io.element.android.features.login.impl.changeserver.ChangeServerView -import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule -import io.element.android.libraries.designsystem.components.BigIcon -import io.element.android.libraries.designsystem.components.button.BackButton -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.Scaffold -import io.element.android.libraries.designsystem.theme.components.TopAppBar -import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding -import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets - -/** - * https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817 - */ -@Composable -fun ChangeAccountProviderView( - state: ChangeAccountProviderState, - onBackClick: () -> Unit, - onLearnMoreClick: () -> Unit, - onSuccess: () -> Unit, - onOtherProviderClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Scaffold( - modifier = modifier, - topBar = { - TopAppBar( - title = {}, - navigationIcon = { BackButton(onClick = onBackClick) } - ) - }, - contentWindowInsets = scaffoldScrollableContentInsets, - ) { padding -> - Box( - modifier = Modifier - .fillMaxSize() - .imePadding() - .padding(padding) - .consumeWindowInsets(padding) - ) { - Column( - modifier = Modifier - .verticalScroll(state = rememberScrollState()) - .padding(lazyColumnContentPadding) - ) { - IconTitleSubtitleMolecule( - modifier = Modifier.padding(top = 16.dp, bottom = 32.dp, start = 16.dp, end = 16.dp), - iconStyle = BigIcon.Style.Default(CompoundIcons.HomeSolid()), - title = stringResource(id = R.string.screen_change_account_provider_title), - subTitle = stringResource(id = R.string.screen_change_account_provider_subtitle), - ) - - state.accountProviders.forEach { item -> - val alteredItem = if (item.isMatrixOrg) { - // Set the subtitle from the resource - item.copy( - subtitle = stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle), - ) - } else { - item - } - AccountProviderView( - item = alteredItem, - onClick = { - state.changeServerState.eventSink.invoke(ChangeServerEvents.ChangeServer(alteredItem)) - } - ) - } - // Other - if (state.canSearchForAccountProviders) { - AccountProviderOtherView( - onClick = onOtherProviderClick - ) - } - Spacer(Modifier.height(32.dp)) - } - ChangeServerView( - state = state.changeServerState, - onLearnMoreClick = onLearnMoreClick, - onSuccess = onSuccess, - ) - } - } -} - -@PreviewsDayNight -@Composable -internal fun ChangeAccountProviderViewPreview(@PreviewParameter(ChangeAccountProviderStateProvider::class) state: ChangeAccountProviderState) = ElementPreview { - ChangeAccountProviderView( - state = state, - onBackClick = { }, - onLearnMoreClick = { }, - onSuccess = { }, - onOtherProviderClick = { }, - ) -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderEvent.kt similarity index 69% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderEvents.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderEvent.kt index f60cc3e9da..bb50f123cb 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderEvent.kt @@ -10,8 +10,8 @@ package io.element.android.features.login.impl.screens.chooseaccountprovider import io.element.android.features.login.impl.accountprovider.AccountProvider -sealed interface ChooseAccountProviderEvents { - data class SelectAccountProvider(val accountProvider: AccountProvider) : ChooseAccountProviderEvents - data object Continue : ChooseAccountProviderEvents - data object ClearError : ChooseAccountProviderEvents +sealed interface ChooseAccountProviderEvent { + data class SelectAccountProvider(val accountProvider: AccountProvider) : ChooseAccountProviderEvent + data object Continue : ChooseAccountProviderEvent + data object ClearError : ChooseAccountProviderEvent } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt index 92c3ff0fff..e2863a295a 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt @@ -34,9 +34,9 @@ class ChooseAccountProviderPresenter( val loginModeState = loginModePresenter.present() var selectedAccountProvider: AccountProvider? by remember { mutableStateOf(null) } - fun handleEvent(event: ChooseAccountProviderEvents) { + fun handleEvent(event: ChooseAccountProviderEvent) { when (event) { - ChooseAccountProviderEvents.Continue -> { + ChooseAccountProviderEvent.Continue -> { selectedAccountProvider?.let { provider -> loginModeState.eventSink( LoginModeEvent.Submit( @@ -48,13 +48,13 @@ class ChooseAccountProviderPresenter( ) } } - is ChooseAccountProviderEvents.SelectAccountProvider -> { + is ChooseAccountProviderEvent.SelectAccountProvider -> { // Ensure that the user does not change the server during processing. if (loginModeState.loginMode is AsyncData.Uninitialized) { selectedAccountProvider = event.accountProvider } } - ChooseAccountProviderEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError) + ChooseAccountProviderEvent.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt index a31e5c464f..398e32d687 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt @@ -17,7 +17,7 @@ data class ChooseAccountProviderState( val accountProviders: ImmutableList, val selectedAccountProvider: AccountProvider?, val loginModeState: LoginModeState, - val eventSink: (ChooseAccountProviderEvents) -> Unit, + val eventSink: (ChooseAccountProviderEvent) -> Unit, ) { val submitEnabled: Boolean get() = selectedAccountProvider != null && diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStatePreviewParam.kt similarity index 94% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStatePreviewParam.kt index 3fea548fca..ceb1cfd6ea 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStatePreviewParam.kt @@ -16,7 +16,7 @@ import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.architecture.AsyncData import kotlinx.collections.immutable.toImmutableList -open class ChooseAccountProviderStateProvider : PreviewParameterProvider { +open class ChooseAccountProviderStatePreviewParam : PreviewParameterProvider { private val server1 = anAccountProvider( url = "https://server1.io", subtitle = null, @@ -71,7 +71,7 @@ fun aChooseAccountProviderState( ), selectedAccountProvider: AccountProvider? = null, loginModeState: LoginModeState = aLoginModeState(), - eventSink: (ChooseAccountProviderEvents) -> Unit = {}, + eventSink: (ChooseAccountProviderEvent) -> Unit = {}, ) = ChooseAccountProviderState( accountProviders = accountProviders.toImmutableList(), selectedAccountProvider = selectedAccountProvider, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt index 0d9d8cfba7..552165a7b2 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt @@ -108,7 +108,7 @@ fun ChooseAccountProviderView( item = alteredItem, selected = item == state.selectedAccountProvider, onClick = { - state.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(item)) + state.eventSink(ChooseAccountProviderEvent.SelectAccountProvider(item)) } ) } @@ -119,7 +119,7 @@ fun ChooseAccountProviderView( text = stringResource(id = CommonStrings.action_continue), showProgress = isLoading, onClick = { - state.eventSink(ChooseAccountProviderEvents.Continue) + state.eventSink(ChooseAccountProviderEvent.Continue) }, enabled = state.submitEnabled || isLoading, modifier = Modifier @@ -131,7 +131,7 @@ fun ChooseAccountProviderView( LoginModeView( loginMode = state.loginModeState.loginMode, onClearError = { - state.eventSink(ChooseAccountProviderEvents.ClearError) + state.eventSink(ChooseAccountProviderEvent.ClearError) }, onLearnMoreClick = onLearnMoreClick, onOAuthDetails = onOAuthDetails, @@ -152,7 +152,9 @@ fun ChooseAccountProviderView( @PreviewsDayNight @Composable -internal fun ChooseAccountProviderViewPreview(@PreviewParameter(ChooseAccountProviderStateProvider::class) state: ChooseAccountProviderState) = ElementPreview { +internal fun ChooseAccountProviderViewPreview(@PreviewParameter( + ChooseAccountProviderStatePreviewParam::class +) state: ChooseAccountProviderState) = ElementPreview { ChooseAccountProviderView( state = state, onBackClick = { }, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStatePreviewParam.kt similarity index 94% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStatePreviewParam.kt index 6dfc292f6f..056d3d704a 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStatePreviewParam.kt @@ -15,7 +15,7 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.matrix.api.core.UserId -open class LoginWithClassicStateProvider : PreviewParameterProvider { +open class LoginWithClassicStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aLoginWithClassicState(), diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt index 81638b4be0..845c8ec8ee 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt @@ -217,7 +217,7 @@ fun LoginWithClassicView( @PreviewsDayNight @Composable -internal fun LoginWithClassicViewPreview(@PreviewParameter(LoginWithClassicStateProvider::class) state: LoginWithClassicState) = ElementPreview { +internal fun LoginWithClassicViewPreview(@PreviewParameter(LoginWithClassicStatePreviewParam::class) state: LoginWithClassicState) = ElementPreview { LoginWithClassicView( state = state, onOtherOptionsClick = {}, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupStatePreviewParam.kt similarity index 86% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupStateProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupStatePreviewParam.kt index 2c6a09b3ed..08d26bcee1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupStatePreviewParam.kt @@ -9,7 +9,7 @@ package io.element.android.features.login.impl.screens.classic.missingkeybackup import androidx.compose.ui.tooling.preview.PreviewParameterProvider -open class MissingKeyBackupStateProvider : PreviewParameterProvider { +open class MissingKeyBackupStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMissingKeyBackupState(), diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupView.kt index c4c9c5f286..9297fc4a51 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/missingkeybackup/MissingKeyBackupView.kt @@ -76,7 +76,7 @@ private fun ColumnScope.Buttons( @PreviewsDayNight @Composable -internal fun MissingKeyBackupViewPreview(@PreviewParameter(MissingKeyBackupStateProvider::class) state: MissingKeyBackupState) = ElementPreview { +internal fun MissingKeyBackupViewPreview(@PreviewParameter(MissingKeyBackupStatePreviewParam::class) state: MissingKeyBackupState) = ElementPreview { MissingKeyBackupView( state = state, onBackClick = {}, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvent.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvent.kt new file mode 100644 index 0000000000..24b6fdfcda --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvent.kt @@ -0,0 +1,18 @@ +/* + * 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.features.login.impl.screens.confirmaccountprovider + +sealed interface ConfirmAccountProviderEvent { + data class UserInputChanged(val accountProvider: String) : ConfirmAccountProviderEvent + + // Carries the account provider to submit (the current field text, or the accepted autocomplete suggestion), + // captured from the view at click time so it can't lag behind fast input (e.g. keyboard autofill). + data class Continue(val accountProvider: String) : ConfirmAccountProviderEvent + data object ClearError : ConfirmAccountProviderEvent +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvents.kt deleted file mode 100644 index d66606b22c..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvents.kt +++ /dev/null @@ -1,14 +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.features.login.impl.screens.confirmaccountprovider - -sealed interface ConfirmAccountProviderEvents { - data object Continue : ConfirmAccountProviderEvents - data object ClearError : ConfirmAccountProviderEvents -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt index 0accaea767..e1a2d85412 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt @@ -45,7 +45,6 @@ class ConfirmAccountProviderNode( interface Callback : Plugin { fun navigateToLoginPassword() fun navigateToOAuth(oAuthDetails: OAuthDetails) - fun navigateToChangeAccountProvider() } private val callback: Callback = callback() @@ -59,7 +58,6 @@ class ConfirmAccountProviderNode( modifier = modifier, onOAuthDetails = callback::navigateToOAuth, onNeedLoginPassword = callback::navigateToLoginPassword, - onChange = callback::navigateToChangeAccountProvider, onLearnMoreClick = { openLearnMorePage(context) }, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt index 0111b1e7d7..627620ff31 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt @@ -9,21 +9,38 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject +import io.element.android.appconfig.AuthenticationConfig +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.changeserver.ChangeServerEvent +import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.uri.ensureProtocol +import io.element.android.libraries.matrix.api.core.MatrixPatterns +import io.element.android.libraries.preferences.api.store.AppPreferencesStore @AssistedInject class ConfirmAccountProviderPresenter( @Assisted private val params: Params, private val accountProviderDataSource: AccountProviderDataSource, private val loginModePresenter: Presenter, + private val changeServerPresenter: Presenter, + private val appPreferencesStore: AppPreferencesStore, + private val enterpriseService: EnterpriseService, ) : Presenter { data class Params( val isAccountCreation: Boolean, @@ -38,26 +55,108 @@ class ConfirmAccountProviderPresenter( override fun present(): ConfirmAccountProviderState { val accountProvider by accountProviderDataSource.flow.collectAsState() val loginModeState = loginModePresenter.present() + val changeServerState = changeServerPresenter.present() + val homeserverHistory by appPreferencesStore.getHomeserverHistoryFlow().collectAsState(emptyList()) - fun handleEvent(event: ConfirmAccountProviderEvents) { - when (event) { - ConfirmAccountProviderEvents.Continue -> loginModeState.eventSink( + // The account providers offered for autocomplete: previously-used ones first, then the + // enterprise/MDM-configured allow-list, then matrix.org (always available, even before any sign-in). + // The "*" wildcard is a routing marker, not a real provider, so it is filtered out. Everything is + // rendered without the https:// scheme (added back at connection time). + val autocompleteCandidates = remember(homeserverHistory) { + (homeserverHistory + enterpriseService.homeserverAllowList() + AuthenticationConfig.MATRIX_ORG_URL) + .filter { it != EnterpriseService.ANY_ACCOUNT_PROVIDER } + .map { it.withoutScheme() } + .distinct() + } + + // Editable input, seeded from the current (history-defaulted) account provider until the user edits it. + // Displayed without the scheme, so the field shows e.g. "matrix.org" rather than "https://matrix.org". + var userInput by rememberSaveable { mutableStateOf(null) } + val accountProviderInput = userInput ?: accountProvider.url.withoutScheme() + + // Offer the first candidate that the current input is a (case-insensitive) prefix of. + val accountProviderSuggestion = remember(accountProviderInput, autocompleteCandidates) { + val input = accountProviderInput.trim() + input.takeIf { it.isNotEmpty() } + ?.let { prefix -> + autocompleteCandidates.firstOrNull { it.length > prefix.length && it.startsWith(prefix, ignoreCase = true) } + } + } + + // The account provider submitted via Continue (carried by the event, so it reflects the exact field + // text at click time); used to actually sign in once validation succeeds. + var submittedAccountProvider by remember { mutableStateOf(null) } + val latestSubmittedAccountProvider by rememberUpdatedState(submittedAccountProvider) + + // A login hint (the full Matrix user ID) captured when the user enters one, so the authentication + // server can pre-fill their identity. Null when a plain account provider was entered. + var submittedLoginHint by remember { mutableStateOf(null) } + val latestSubmittedLoginHint by rememberUpdatedState(submittedLoginHint) + + // Once the chosen account provider has been validated and persisted, proceed with the actual sign in / sign up. + // Reuse the details resolved while validating, so login does not configure (and re-network) the homeserver again. + LaunchedEffect(changeServerState.changeServerAction) { + val homeServerDetails = changeServerState.changeServerAction.dataOrNull() + if (homeServerDetails != null) { + loginModeState.eventSink( LoginModeEvent.Submit( isAccountCreation = params.isAccountCreation, - homeserverUrl = accountProvider.url, + homeserverUrl = latestSubmittedAccountProvider.orEmpty().trim(), resolvedHomeserverUrl = null, - loginHint = null, + loginHint = latestSubmittedLoginHint, + preConfiguredDetails = homeServerDetails, ) ) - ConfirmAccountProviderEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError) + } + } + + fun handleEvent(event: ConfirmAccountProviderEvent) { + when (event) { + is ConfirmAccountProviderEvent.UserInputChanged -> { + userInput = event.accountProvider + } + // Validate (and persist) the chosen account provider before proceeding. This also enforces the + // account-provider access control, which the login submit does not run on its own. + is ConfirmAccountProviderEvent.Continue -> { + // Apply the accepted account provider (any accepted completion) back into the field so it + // renders the full server rather than the typed prefix, and survives the OAuth round-trip. + val accountProvider = event.accountProvider.trim() + userInput = accountProvider + // If the user entered a full Matrix user ID (e.g. "@alice:example.org"), sign in to its + // homeserver and pass the ID as a login hint so the authentication server can pre-fill it. + // Otherwise treat the input as the account provider host. Only the homeserver is ever + // persisted to history, never the user ID. + val isUserId = MatrixPatterns.isUserId(accountProvider) + // For a user ID the homeserver is the domain part (after the ':'), keeping any port. + val host = if (isUserId) accountProvider.substringAfter(":") else accountProvider + submittedLoginHint = "mxid:$accountProvider".takeIf { isUserId && !params.isAccountCreation } + // Default the scheme to https:// so entering a bare host (e.g. "matrix.org") works. + val accountProviderUrl = host.ensureProtocol() + submittedAccountProvider = accountProviderUrl + changeServerState.eventSink( + ChangeServerEvent.ChangeServer(AccountProvider(url = accountProviderUrl)) + ) + } + ConfirmAccountProviderEvent.ClearError -> { + loginModeState.eventSink(LoginModeEvent.ClearError) + changeServerState.eventSink(ChangeServerEvent.ClearError) + } } } return ConfirmAccountProviderState( - accountProvider = accountProvider, + accountProviderInput = accountProviderInput, + accountProviderSuggestion = accountProviderSuggestion, isAccountCreation = params.isAccountCreation, loginModeState = loginModeState, + changeServerState = changeServerState, eventSink = ::handleEvent, ) } } + +/** + * The host part of an account provider URL, i.e. the value with any `https://` / `http://` scheme removed. + * Returns the input unchanged when it has no scheme (e.g. `matrix.org`). + */ +private fun String.withoutScheme(): String = substringAfter("://") diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt index 6e6deb8c2c..eec082d886 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt @@ -8,17 +8,29 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider -import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.changeserver.ChangeServerState import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncData data class ConfirmAccountProviderState( - val accountProvider: AccountProvider, + val accountProviderInput: String, + // The full account provider from history that the current input is a prefix of, offered as inline autocomplete. + val accountProviderSuggestion: String?, val isAccountCreation: Boolean, val loginModeState: LoginModeState, - val eventSink: (ConfirmAccountProviderEvents) -> Unit + val changeServerState: ChangeServerState, + val eventSink: (ConfirmAccountProviderEvent) -> Unit ) { val submitEnabled: Boolean - get() = accountProvider.url.isNotEmpty() && - (loginModeState.loginMode is AsyncData.Uninitialized || loginModeState.loginMode is AsyncData.Loading) + get() = accountProviderInput.isNotBlank() && + loginModeState.loginMode is AsyncData.Uninitialized && + changeServerState.changeServerAction is AsyncData.Uninitialized + + val isLoading: Boolean + get() = loginModeState.loginMode is AsyncData.Loading || + changeServerState.changeServerAction is AsyncData.Loading + + val isShowingError: Boolean + get() = loginModeState.loginMode is AsyncData.Failure || + changeServerState.changeServerAction is AsyncData.Failure } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStatePreviewParam.kt similarity index 63% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStatePreviewParam.kt index f597ad6b54..be85a306ba 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStatePreviewParam.kt @@ -9,17 +9,21 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.accountprovider.AccountProvider -import io.element.android.features.login.impl.accountprovider.anAccountProvider +import io.element.android.features.login.impl.changeserver.ChangeServerState +import io.element.android.features.login.impl.changeserver.aChangeServerState import io.element.android.features.login.impl.login.LoginModeState import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported import io.element.android.libraries.architecture.AsyncData -open class ConfirmAccountProviderStateProvider : PreviewParameterProvider { +open class ConfirmAccountProviderStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aConfirmAccountProviderState(), + aConfirmAccountProviderState( + accountProviderInput = "element", + accountProviderSuggestion = "element.io", + ), aConfirmAccountProviderState( isAccountCreation = true, ), @@ -31,13 +35,17 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider Unit = {}, + changeServerState: ChangeServerState = aChangeServerState(), + eventSink: (ConfirmAccountProviderEvent) -> Unit = {}, ) = ConfirmAccountProviderState( - accountProvider = accountProvider, + accountProviderInput = accountProviderInput, + accountProviderSuggestion = accountProviderSuggestion, isAccountCreation = isAccountCreation, loginModeState = loginModeState, + changeServerState = changeServerState, eventSink = eventSink ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt index 3702b195a1..3f08b269a1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt @@ -8,107 +8,229 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.withStyle 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.login.impl.R +import io.element.android.features.login.impl.changeserver.ChangeServerView import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeView -import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.components.BigIcon +import io.element.android.libraries.designsystem.components.form.textFieldState import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button -import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.matrix.api.auth.OAuthDetails import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings +/** + * Account provider entry screen, shared by sign in and account creation. + * Figma (Compound Android Templates): + * - Sign in: https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=41-10251 + * - Create account: https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=41-10239 + */ @Composable fun ConfirmAccountProviderView( state: ConfirmAccountProviderState, onOAuthDetails: (OAuthDetails) -> Unit, onNeedLoginPassword: () -> Unit, onLearnMoreClick: () -> Unit, - onChange: () -> Unit, modifier: Modifier = Modifier, ) { - val isLoading by remember(state.loginModeState.loginMode) { - derivedStateOf { - state.loginModeState.loginMode is AsyncData.Loading - } - } val eventSink = state.eventSink + val focusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + var input by textFieldState(stateValue = state.accountProviderInput) + + // Inline autocomplete: the un-typed remainder of the suggested account provider (rendered in grey and + // accepted on Continue / Enter). Only valid while the suggestion still starts with the current input. + val suggestionSuffix = state.accountProviderSuggestion + ?.takeIf { it.length > input.length && it.startsWith(input, ignoreCase = true) } + ?.substring(input.length) + .orEmpty() + + // The Continue button lives in a footer slot that is not recomposed while the user types, so a direct + // capture of the field text (via ::submit) goes stale and can submit an empty/prefilled value. Reading + // the value through rememberUpdatedState keeps it current regardless of which composition scope reads it. + val accountProviderToSubmit by rememberUpdatedState(input + suggestionSuffix) + + fun submit() { + // Dismiss the keyboard and release focus while the account provider is being validated. + focusManager.clearFocus(force = true) + // Submit the exact field text (plus any accepted suggestion). + eventSink(ConfirmAccountProviderEvent.Continue(accountProviderToSubmit)) + } + + // Once a validation / login error has been dismissed, return focus and the keyboard to the field so + // the user can correct the account provider and retry. + var wasShowingError by remember { mutableStateOf(false) } + LaunchedEffect(state.isShowingError) { + if (wasShowingError && !state.isShowingError) { + focusRequester.requestFocus() + keyboardController?.show() + } + wasShowingError = state.isShowingError + } HeaderFooterPage( modifier = modifier, + // Let the header and content scroll so the supporting text is not clipped by the footer when the + // keyboard is visible and vertical space is reduced. + isScrollable = true, header = { IconTitleSubtitleMolecule( modifier = Modifier.padding(top = 60.dp), iconStyle = BigIcon.Style.Default(CompoundIcons.UserProfileSolid()), title = stringResource( id = if (state.isAccountCreation) { - R.string.screen_account_provider_signup_title + R.string.screen_change_server_title_register } else { - R.string.screen_account_provider_signin_title - }, - state.accountProvider.title + R.string.screen_change_server_title_login + } ), - subTitle = stringResource( - id = if (state.isAccountCreation) { - R.string.screen_account_provider_signup_subtitle - } else { - R.string.screen_account_provider_signin_subtitle - }, - ) + subTitle = null, ) }, footer = { ButtonColumnMolecule { Button( text = stringResource(id = CommonStrings.action_continue), - showProgress = isLoading, - onClick = { eventSink.invoke(ConfirmAccountProviderEvents.Continue) }, - enabled = state.submitEnabled || isLoading, + showProgress = state.isLoading, + onClick = ::submit, + enabled = state.submitEnabled && !state.isLoading, modifier = Modifier .fillMaxWidth() .testTag(TestTags.loginContinue) ) - TextButton( - text = stringResource(id = R.string.screen_account_provider_change), - onClick = onChange, - enabled = !isLoading, - modifier = Modifier - .fillMaxWidth() - .testTag(TestTags.loginChangeServer) - ) } } ) { - LoginModeView( - loginMode = state.loginModeState.loginMode, - onClearError = { - eventSink(ConfirmAccountProviderEvents.ClearError) + val ghostColor = ElementTheme.colors.textSecondary + val ghostTransformation = remember(suggestionSuffix, ghostColor) { + GhostSuffixVisualTransformation(suggestionSuffix, ghostColor) + } + TextField( + value = input, + onValueChange = { + input = it + eventSink(ConfirmAccountProviderEvent.UserInputChanged(it)) + }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 40.dp) + .focusRequester(focusRequester) + .testTag(TestTags.changeServerServer), + label = stringResource(id = R.string.screen_change_server_textfield_header), + placeholder = stringResource(id = R.string.screen_change_server_textfield_placeholder), + supportingText = stringResource( + id = if (state.isAccountCreation) { + R.string.screen_change_server_textfield_footer_register + } else { + R.string.screen_change_server_textfield_footer_login + } + ), + visualTransformation = ghostTransformation, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions( + onDone = { + if (suggestionSuffix.isNotEmpty()) { + // A completion is showing: accept it into the field and drop focus, dismissing the + // keyboard. This shows the completed server (with no stray mid-text cursor, since the + // field is no longer focused) ready to sign in with Continue. When no completion is + // showing, Done submits directly. + val accepted = input + suggestionSuffix + input = accepted + eventSink(ConfirmAccountProviderEvent.UserInputChanged(accepted)) + focusManager.clearFocus(force = true) + } else { + submit() + } + } + ), + singleLine = true, + trailingIcon = if (input.isNotEmpty()) { + { + Box( + Modifier.clickable( + onClickLabel = stringResource(CommonStrings.action_clear), + role = Role.Button, + ) { + input = "" + eventSink(ConfirmAccountProviderEvent.UserInputChanged("")) + } + ) { + Icon( + imageVector = CompoundIcons.Close(), + contentDescription = stringResource(CommonStrings.action_clear) + ) + } + } + } else { + null }, - onLearnMoreClick = onLearnMoreClick, - onOAuthDetails = onOAuthDetails, - onNeedLoginPassword = onNeedLoginPassword, ) } + // Renders the account-provider validation errors, progress and permission dialogs. + // The successful validation itself is observed by the presenter, which then proceeds with the login flow. + ChangeServerView( + state = state.changeServerState, + onLearnMoreClick = onLearnMoreClick, + onSuccess = {}, + ) + + LoginModeView( + loginMode = state.loginModeState.loginMode, + onClearError = { eventSink(ConfirmAccountProviderEvent.ClearError) }, + onLearnMoreClick = onLearnMoreClick, + onOAuthDetails = onOAuthDetails, + onNeedLoginPassword = onNeedLoginPassword, + ) + LocalNetworkPermissionDialogView( dialog = state.loginModeState.localNetworkPermissionDialog, onSubmit = { @@ -120,16 +242,38 @@ fun ConfirmAccountProviderView( ) } +/** + * Appends [suffix] (in [color]) after the user's text as a non-editable inline autocomplete hint. + * The underlying field value stays limited to what the user actually typed, so the cursor can never + * enter the suggested region. + */ +private class GhostSuffixVisualTransformation( + private val suffix: String, + private val color: Color, +) : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + if (suffix.isEmpty()) return TransformedText(text, OffsetMapping.Identity) + val transformed = buildAnnotatedString { + append(text) + withStyle(SpanStyle(color = color)) { append(suffix) } + } + val offsetMapping = object : OffsetMapping { + override fun originalToTransformed(offset: Int) = offset + override fun transformedToOriginal(offset: Int) = offset.coerceAtMost(text.length) + } + return TransformedText(transformed, offsetMapping) + } +} + @PreviewsDayNight @Composable internal fun ConfirmAccountProviderViewPreview( - @PreviewParameter(ConfirmAccountProviderStateProvider::class) state: ConfirmAccountProviderState + @PreviewParameter(ConfirmAccountProviderStatePreviewParam::class) state: ConfirmAccountProviderState ) = ElementPreview { ConfirmAccountProviderView( state = state, onOAuthDetails = {}, onNeedLoginPassword = {}, onLearnMoreClick = {}, - onChange = {}, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvent.kt similarity index 64% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvents.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvent.kt index 25a003b531..fa9acb5632 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvent.kt @@ -8,9 +8,9 @@ package io.element.android.features.login.impl.screens.loginpassword -sealed interface LoginPasswordEvents { - data class SetLogin(val login: String) : LoginPasswordEvents - data class SetPassword(val password: String) : LoginPasswordEvents - data object Submit : LoginPasswordEvents - data object ClearError : LoginPasswordEvents +sealed interface LoginPasswordEvent { + data class SetLogin(val login: String) : LoginPasswordEvent + data class SetPassword(val password: String) : LoginPasswordEvent + data object Submit : LoginPasswordEvent + data object ClearError : LoginPasswordEvent } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt index f26f342a42..864bbc7808 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt @@ -20,6 +20,7 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.accountprovider.SaveAccountProviderToHistory import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService @@ -33,6 +34,7 @@ class LoginPasswordPresenter( private val initialLogin: String, private val authenticationService: MatrixAuthenticationService, private val accountProviderDataSource: AccountProviderDataSource, + private val saveAccountProviderToHistory: SaveAccountProviderToHistory, ) : Presenter { @AssistedFactory interface Factory { @@ -56,18 +58,18 @@ class LoginPasswordPresenter( } val accountProvider by accountProviderDataSource.flow.collectAsState() - fun handleEvent(event: LoginPasswordEvents) { + fun handleEvent(event: LoginPasswordEvent) { when (event) { - is LoginPasswordEvents.SetLogin -> updateFormState(formState) { + is LoginPasswordEvent.SetLogin -> updateFormState(formState) { copy(login = event.login) } - is LoginPasswordEvents.SetPassword -> updateFormState(formState) { + is LoginPasswordEvent.SetPassword -> updateFormState(formState) { copy(password = event.password) } - LoginPasswordEvents.Submit -> { + LoginPasswordEvent.Submit -> { localCoroutineScope.submit(formState.value, loginAction) } - LoginPasswordEvents.ClearError -> loginAction.value = AsyncData.Uninitialized + LoginPasswordEvent.ClearError -> loginAction.value = AsyncData.Uninitialized } } @@ -83,6 +85,7 @@ class LoginPasswordPresenter( loggedInState.value = AsyncData.Loading() authenticationService.login(formState.login.trim(), formState.password) .onSuccess { sessionId -> + saveAccountProviderToHistory() loggedInState.value = AsyncData.Success(sessionId) } .onFailure { failure -> diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordState.kt index d8adc7351f..db04e7b7ba 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordState.kt @@ -18,7 +18,7 @@ data class LoginPasswordState( val accountProvider: AccountProvider, val formState: LoginFormState, val loginAction: AsyncData, - val eventSink: (LoginPasswordEvents) -> Unit + val eventSink: (LoginPasswordEvent) -> Unit ) { val submitEnabled: Boolean get() = loginAction !is AsyncData.Failure && diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStatePreviewParam.kt similarity index 91% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStateProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStatePreviewParam.kt index 2183790365..ba48e57b5d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStatePreviewParam.kt @@ -14,7 +14,7 @@ import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.SessionId -open class LoginPasswordStateProvider : PreviewParameterProvider { +open class LoginPasswordStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aLoginPasswordState(), @@ -29,7 +29,7 @@ fun aLoginPasswordState( accountProvider: AccountProvider = anAccountProvider(), formState: LoginFormState = LoginFormState.Default, loginAction: AsyncData = AsyncData.Uninitialized, - eventSink: (LoginPasswordEvents) -> Unit = {}, + eventSink: (LoginPasswordEvent) -> Unit = {}, ) = LoginPasswordState( accountProvider = accountProvider, formState = formState, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt index 06a1641644..9a2dc3097b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt @@ -56,6 +56,7 @@ import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.form.textFieldState +import io.element.android.libraries.designsystem.modifiers.bringIntoViewOnImeVisible import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -96,7 +97,7 @@ fun LoginPasswordView( autofillManager?.commit() - state.eventSink(LoginPasswordEvents.Submit) + state.eventSink(LoginPasswordEvent.Submit) } Scaffold( @@ -165,7 +166,7 @@ fun LoginPasswordView( if (state.loginAction is AsyncData.Failure) { LoginErrorDialog(error = state.loginAction.error, onDismiss = { - state.eventSink(LoginPasswordEvents.ClearError) + state.eventSink(LoginPasswordEvent.ClearError) }) } } @@ -191,6 +192,7 @@ private fun LoginForm( enabled = !isLoading, modifier = Modifier .fillMaxWidth() + .bringIntoViewOnImeVisible() .onTabOrEnterKeyFocusNext(focusManager) .testTag(TestTags.loginEmailUsername) .semantics { @@ -200,7 +202,7 @@ private fun LoginForm( onValueChange = { val sanitized = it.sanitize() loginFieldState = sanitized - eventSink(LoginPasswordEvents.SetLogin(sanitized)) + eventSink(LoginPasswordEvent.SetLogin(sanitized)) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Email, @@ -218,7 +220,7 @@ private fun LoginForm( role = Role.Button, ) { loginFieldState = "" - eventSink(LoginPasswordEvents.SetLogin("")) + eventSink(LoginPasswordEvent.SetLogin("")) } ) { Icon( @@ -243,6 +245,7 @@ private fun LoginForm( enabled = !isLoading, modifier = Modifier .fillMaxWidth() + .bringIntoViewOnImeVisible() .onTabOrEnterKeyFocusNext(focusManager) .testTag(TestTags.loginPassword) .semantics { @@ -251,7 +254,7 @@ private fun LoginForm( onValueChange = { val sanitized = it.sanitize() passwordFieldState = sanitized - eventSink(LoginPasswordEvents.SetPassword(sanitized)) + eventSink(LoginPasswordEvent.SetPassword(sanitized)) }, placeholder = stringResource(CommonStrings.common_password), visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), @@ -274,10 +277,10 @@ private fun LoginForm( } /** - * 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' } } @Composable @@ -291,7 +294,7 @@ private fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) { @PreviewsDayNight @Composable -internal fun LoginPasswordViewPreview(@PreviewParameter(LoginPasswordStateProvider::class) state: LoginPasswordState) = ElementPreview { +internal fun LoginPasswordViewPreview(@PreviewParameter(LoginPasswordStatePreviewParam::class) state: LoginPasswordState) = ElementPreview { LoginPasswordView( state = state, onBackClick = {}, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingEvent.kt similarity index 70% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingEvents.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingEvent.kt index 6101b21ded..4b3993e2d1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingEvent.kt @@ -8,11 +8,11 @@ package io.element.android.features.login.impl.screens.onboarding -sealed interface OnBoardingEvents { +sealed interface OnBoardingEvent { data class OnSignIn( val defaultAccountProvider: String - ) : OnBoardingEvents + ) : OnBoardingEvent - data object OnVersionClick : OnBoardingEvents - data object ClearError : OnBoardingEvents + data object OnVersionClick : OnBoardingEvent + data object ClearError : OnBoardingEvent } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt index 850e4332dd..7e48ae22e8 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt @@ -60,7 +60,7 @@ class OnBoardingPresenter( override fun present(): OnBoardingState { val localCoroutineScope = rememberCoroutineScope() val forcedAccountProvider = remember { - // If defaultHomeserverList() returns a singleton list, this is the default account provider. + // If homeserverAllowList() returns a singleton list, this is the default account provider. // In this case, the user can sign in using this homeserver, or use QrCode login enterpriseService.homeserverAllowList().singleOrNull() } @@ -101,9 +101,9 @@ class OnBoardingPresenter( val loginModeState = loginModePresenter.present() - fun handleEvent(event: OnBoardingEvents) { + fun handleEvent(event: OnBoardingEvent) { when (event) { - is OnBoardingEvents.OnSignIn -> localCoroutineScope.launch { + is OnBoardingEvent.OnSignIn -> localCoroutineScope.launch { // Ensure that the current account provider is set accountProviderDataSource.setUrl(event.defaultAccountProvider) loginModeState.eventSink( @@ -115,8 +115,8 @@ class OnBoardingPresenter( ) ) } - OnBoardingEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError) - OnBoardingEvents.OnVersionClick -> { + OnBoardingEvent.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError) + OnBoardingEvent.OnVersionClick -> { if (canReportBug) { if (multipleTapToUnlock.unlock(localCoroutineScope)) { showReportBug = true diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt index 4fb4b8090e..97ebed67f6 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt @@ -26,7 +26,7 @@ data class OnBoardingState( @DrawableRes val onBoardingLogoResId: Int?, val loginModeState: LoginModeState, - val eventSink: (OnBoardingEvents) -> Unit, + val eventSink: (OnBoardingEvent) -> Unit, ) { val submitEnabled: Boolean get() = defaultAccountProvider != null && diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStatePreviewParam.kt similarity index 95% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStatePreviewParam.kt index 41e4a81818..c7d89b21d5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStatePreviewParam.kt @@ -14,7 +14,7 @@ import io.element.android.features.login.impl.login.LoginModeState import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.designsystem.R -open class OnBoardingStateProvider : PreviewParameterProvider { +open class OnBoardingStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( anOnBoardingState(), @@ -50,7 +50,7 @@ fun anOnBoardingState( @DrawableRes customLogoResId: Int? = null, loginModeState: LoginModeState = aLoginModeState(), - eventSink: (OnBoardingEvents) -> Unit = {}, + eventSink: (OnBoardingEvent) -> Unit = {}, ) = OnBoardingState( isAddingAccount = isAddingAccount, showBackButton = showBackButton, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt index 10fe3a854c..bd257d0891 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt @@ -78,7 +78,7 @@ fun OnBoardingView( LoginModeView( loginMode = state.loginModeState.loginMode, onClearError = { - state.eventSink(OnBoardingEvents.ClearError) + state.eventSink(OnBoardingEvent.ClearError) }, onLearnMoreClick = onLearnMoreClick, onOAuthDetails = onOAuthDetails, @@ -306,7 +306,7 @@ private fun OnBoardingButtons( text = stringResource(id = R.string.screen_onboarding_sign_in_to, defaultAccountProvider), showProgress = isLoading, onClick = { - state.eventSink(OnBoardingEvents.OnSignIn(defaultAccountProvider)) + state.eventSink(OnBoardingEvent.OnSignIn(defaultAccountProvider)) }, enabled = state.submitEnabled || isLoading, modifier = Modifier @@ -336,7 +336,7 @@ private fun OnBoardingButtons( Text( modifier = Modifier .clickable(role = Role.Button) { - state.eventSink(OnBoardingEvents.OnVersionClick) + state.eventSink(OnBoardingEvent.OnVersionClick) } .padding(16.dp), text = stringResource(id = R.string.screen_onboarding_app_version, state.version), @@ -351,7 +351,7 @@ private fun OnBoardingButtons( @PreviewsDayNight @Composable internal fun OnBoardingViewPreview( - @PreviewParameter(OnBoardingStateProvider::class) state: OnBoardingState + @PreviewParameter(OnBoardingStatePreviewParam::class) state: OnBoardingState ) = ElementPreview { OnBoardingView( state = state, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationStepProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationStepPreviewParam.kt similarity index 87% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationStepProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationStepPreviewParam.kt index 17864f68a1..700c6a87b7 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationStepProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationStepPreviewParam.kt @@ -10,7 +10,7 @@ package io.element.android.features.login.impl.screens.qrcode.confirmation import androidx.compose.ui.tooling.preview.PreviewParameterProvider -class QrCodeConfirmationStepProvider : PreviewParameterProvider { +class QrCodeConfirmationStepPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( QrCodeConfirmationStep.DisplayCheckCode("12"), diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationView.kt index 8de93bcb0f..2c29f4a667 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/confirmation/QrCodeConfirmationView.kt @@ -148,7 +148,7 @@ private fun Buttons( @PreviewsDayNight @Composable -internal fun QrCodeConfirmationViewPreview(@PreviewParameter(QrCodeConfirmationStepProvider::class) step: QrCodeConfirmationStep) { +internal fun QrCodeConfirmationViewPreview(@PreviewParameter(QrCodeConfirmationStepPreviewParam::class) step: QrCodeConfirmationStep) { ElementPreview { QrCodeConfirmationView( step = step, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorView.kt index 6b5da8f300..4d58b9329a 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/error/QrCodeErrorView.kt @@ -143,7 +143,7 @@ private fun ColumnScope.Buttons( @PreviewsDayNight @Composable -internal fun QrCodeErrorViewPreview(@PreviewParameter(QrCodeErrorScreenTypeProvider::class) errorScreenType: QrCodeErrorScreenType) { +internal fun QrCodeErrorViewPreview(@PreviewParameter(QrCodeErrorScreenTypePreviewParam::class) errorScreenType: QrCodeErrorScreenType) { ElementPreview { QrCodeErrorView( errorScreenType = errorScreenType, @@ -154,7 +154,7 @@ internal fun QrCodeErrorViewPreview(@PreviewParameter(QrCodeErrorScreenTypeProvi } } -class QrCodeErrorScreenTypeProvider : PreviewParameterProvider { +class QrCodeErrorScreenTypePreviewParam : PreviewParameterProvider { override val values: Sequence = sequenceOf( QrCodeErrorScreenType.Cancelled, QrCodeErrorScreenType.Declined, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroEvent.kt similarity index 79% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroEvents.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroEvent.kt index 11abd63683..a4471d8a92 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroEvent.kt @@ -8,6 +8,6 @@ package io.element.android.features.login.impl.screens.qrcode.intro -sealed interface QrCodeIntroEvents { - data object Continue : QrCodeIntroEvents +sealed interface QrCodeIntroEvent { + data object Continue : QrCodeIntroEvent } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenter.kt index 13888fe23f..0781942216 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenter.kt @@ -40,9 +40,9 @@ class QrCodeIntroPresenter( } } - fun handleEvent(event: QrCodeIntroEvents) { + fun handleEvent(event: QrCodeIntroEvent) { when (event) { - QrCodeIntroEvents.Continue -> if (cameraPermissionState.permissionGranted) { + QrCodeIntroEvent.Continue -> if (cameraPermissionState.permissionGranted) { canContinue = true } else { pendingPermissionRequest = true diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroState.kt index d20a3f58d9..ab84743404 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroState.kt @@ -15,5 +15,5 @@ data class QrCodeIntroState( val desktopAppName: String, val cameraPermissionState: PermissionsState, val canContinue: Boolean, - val eventSink: (QrCodeIntroEvents) -> Unit + val eventSink: (QrCodeIntroEvent) -> Unit ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroStatePreviewParam.kt similarity index 90% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroStateProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroStatePreviewParam.kt index 22c7fd67f6..5e5fbac8b1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroStatePreviewParam.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.permissions.api.PermissionsState import io.element.android.libraries.permissions.api.aPermissionsState -open class QrCodeIntroStateProvider : PreviewParameterProvider { +open class QrCodeIntroStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aQrCodeIntroState(), @@ -30,7 +30,7 @@ fun aQrCodeIntroState( permission = Manifest.permission.CAMERA, ), canContinue: Boolean = false, - eventSink: (QrCodeIntroEvents) -> Unit = {}, + eventSink: (QrCodeIntroEvent) -> Unit = {}, ) = QrCodeIntroState( appName = appName, desktopAppName = desktopAppName, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroView.kt index 767dc49999..2691c0cbf5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroView.kt @@ -91,14 +91,14 @@ private fun ColumnScope.Buttons( text = stringResource(id = R.string.screen_qr_code_login_initial_state_button_title), modifier = Modifier.fillMaxWidth(), onClick = { - state.eventSink.invoke(QrCodeIntroEvents.Continue) + state.eventSink.invoke(QrCodeIntroEvent.Continue) } ) } @PreviewsDayNight @Composable -internal fun QrCodeIntroViewPreview(@PreviewParameter(QrCodeIntroStateProvider::class) state: QrCodeIntroState) = ElementPreview { +internal fun QrCodeIntroViewPreview(@PreviewParameter(QrCodeIntroStatePreviewParam::class) state: QrCodeIntroState) = ElementPreview { QrCodeIntroView( state = state, onBackClick = {}, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanEvent.kt similarity index 81% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanEvents.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanEvent.kt index f5804aceb0..d1c91b9b42 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanEvent.kt @@ -8,7 +8,7 @@ package io.element.android.features.login.impl.screens.qrcode.scan -sealed interface QrCodeScanEvents { - data class QrCodeScanned(val code: ByteArray) : QrCodeScanEvents - data object TryAgain : QrCodeScanEvents +sealed interface QrCodeScanEvent { + data class QrCodeScanned(val code: ByteArray) : QrCodeScanEvent + data object TryAgain : QrCodeScanEvent } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenter.kt index 5e298604a4..4075a9d2cd 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenter.kt @@ -52,13 +52,13 @@ class QrCodeScanPresenter( authenticationAction.value = AsyncAction.Failure(it) } - fun handleEvent(event: QrCodeScanEvents) { + fun handleEvent(event: QrCodeScanEvent) { when (event) { - QrCodeScanEvents.TryAgain -> { + QrCodeScanEvent.TryAgain -> { isScanning = true authenticationAction.value = AsyncAction.Uninitialized } - is QrCodeScanEvents.QrCodeScanned -> { + is QrCodeScanEvent.QrCodeScanned -> { isScanning = false coroutineScope.getQrCodeData(authenticationAction, event.code) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanState.kt index 0e64bfc6a6..5d9fca2543 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanState.kt @@ -14,5 +14,5 @@ import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginDat data class QrCodeScanState( val isScanning: Boolean, val authenticationAction: AsyncAction, - val eventSink: (QrCodeScanEvents) -> Unit + val eventSink: (QrCodeScanEvent) -> Unit ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanStatePreviewParam.kt similarity index 94% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanStateProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanStatePreviewParam.kt index 0d467efb71..24164b94c9 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanStatePreviewParam.kt @@ -14,7 +14,7 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.auth.qrlogin.MatrixQrCodeLoginData import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException -open class QrCodeScanStateProvider : PreviewParameterProvider { +open class QrCodeScanStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aQrCodeScanState(), @@ -46,7 +46,7 @@ open class QrCodeScanStateProvider : PreviewParameterProvider { fun aQrCodeScanState( isScanning: Boolean = true, authenticationAction: AsyncAction = AsyncAction.Uninitialized, - eventSink: (QrCodeScanEvents) -> Unit = {}, + eventSink: (QrCodeScanEvent) -> Unit = {}, ) = QrCodeScanState( isScanning = isScanning, authenticationAction = authenticationAction, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanView.kt index 6fa3d1b0c7..372a46788b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanView.kt @@ -105,7 +105,7 @@ private fun Content( ) { QrCodeCameraView( modifier = Modifier.fillMaxSize(), - onScanQrCode = { state.eventSink.invoke(QrCodeScanEvents.QrCodeScanned(it)) }, + onScanQrCode = { state.eventSink.invoke(QrCodeScanEvent.QrCodeScanned(it)) }, isScanning = state.isScanning, ) } @@ -125,7 +125,7 @@ private fun ColumnScope.Buttons( .fillMaxWidth() .padding(bottom = 16.dp), onClick = { - state.eventSink.invoke(QrCodeScanEvents.TryAgain) + state.eventSink.invoke(QrCodeScanEvent.TryAgain) } ) Column( @@ -218,7 +218,7 @@ private fun ColumnScope.Buttons( @PreviewsDayNight @Composable -internal fun QrCodeScanViewPreview(@PreviewParameter(QrCodeScanStateProvider::class) state: QrCodeScanState) = ElementPreview { +internal fun QrCodeScanViewPreview(@PreviewParameter(QrCodeScanStatePreviewParam::class) state: QrCodeScanState) = ElementPreview { QrCodeScanView( state = state, onQrCodeDataReady = {}, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderEvents.kt deleted file mode 100644 index 8816de3946..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderEvents.kt +++ /dev/null @@ -1,17 +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.features.login.impl.screens.searchaccountprovider - -sealed interface SearchAccountProviderEvents { - /** - * The user has typed something, expect to get a list of matching account provider results - * in the state. - */ - data class UserInput(val input: String) : SearchAccountProviderEvents -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderNode.kt deleted file mode 100644 index ddbcc4c847..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderNode.kt +++ /dev/null @@ -1,49 +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.features.login.impl.screens.searchaccountprovider - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -import dev.zacsweers.metro.AppScope -import dev.zacsweers.metro.Assisted -import dev.zacsweers.metro.AssistedInject -import io.element.android.annotations.ContributesNode -import io.element.android.features.login.impl.util.openLearnMorePage -import io.element.android.libraries.architecture.callback - -@ContributesNode(AppScope::class) -@AssistedInject -class SearchAccountProviderNode( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - private val presenter: SearchAccountProviderPresenter, -) : Node(buildContext, plugins = plugins) { - interface Callback : Plugin { - fun onDone() - } - - private val callback: Callback = callback() - - @Composable - override fun View(modifier: Modifier) { - val state = presenter.present() - val context = LocalContext.current - SearchAccountProviderView( - state = state, - modifier = modifier, - onBackClick = ::navigateUp, - onLearnMoreClick = { openLearnMorePage(context) }, - onSuccess = callback::onDone, - ) - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt deleted file mode 100644 index 2efe4501c6..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt +++ /dev/null @@ -1,77 +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.features.login.impl.screens.searchaccountprovider - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import dev.zacsweers.metro.Inject -import io.element.android.features.login.impl.changeserver.ChangeServerState -import io.element.android.features.login.impl.resolver.HomeserverData -import io.element.android.features.login.impl.resolver.HomeserverResolver -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.architecture.Presenter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -@Inject -class SearchAccountProviderPresenter( - private val homeserverResolver: HomeserverResolver, - private val changeServerPresenter: Presenter, -) : Presenter { - @Composable - override fun present(): SearchAccountProviderState { - var userInput by rememberSaveable { - mutableStateOf("") - } - val changeServerState = changeServerPresenter.present() - - val data: MutableState>> = remember { - mutableStateOf(AsyncData.Uninitialized) - } - - LaunchedEffect(userInput) { - onUserInput(userInput, data) - } - - fun handleEvent(event: SearchAccountProviderEvents) { - when (event) { - is SearchAccountProviderEvents.UserInput -> { - userInput = event.input - } - } - } - - return SearchAccountProviderState( - userInput = userInput, - userInputResult = data.value, - changeServerState = changeServerState, - eventSink = ::handleEvent, - ) - } - - private fun CoroutineScope.onUserInput(userInput: String, data: MutableState>>) = launch { - data.value = AsyncData.Uninitialized - // Debounce - delay(500) - data.value = AsyncData.Loading() - homeserverResolver.resolve(userInput).collect { - data.value = AsyncData.Success(it) - } - if (data.value !is AsyncData.Success) { - data.value = AsyncData.Uninitialized - } - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderState.kt deleted file mode 100644 index dc02a997a6..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderState.kt +++ /dev/null @@ -1,20 +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.features.login.impl.screens.searchaccountprovider - -import io.element.android.features.login.impl.changeserver.ChangeServerState -import io.element.android.features.login.impl.resolver.HomeserverData -import io.element.android.libraries.architecture.AsyncData - -data class SearchAccountProviderState( - val userInput: String, - val userInputResult: AsyncData>, - val changeServerState: ChangeServerState, - val eventSink: (SearchAccountProviderEvents) -> Unit -) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt deleted file mode 100644 index 2a8512c38a..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt +++ /dev/null @@ -1,48 +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.features.login.impl.screens.searchaccountprovider - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.appconfig.AuthenticationConfig -import io.element.android.features.login.impl.changeserver.aChangeServerState -import io.element.android.features.login.impl.resolver.HomeserverData -import io.element.android.libraries.architecture.AsyncData - -open class SearchAccountProviderStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aSearchAccountProviderState(), - aSearchAccountProviderState(userInputResult = AsyncData.Success(aHomeserverDataList())), - // Add other state here - ) -} - -fun aSearchAccountProviderState( - userInput: String = "", - userInputResult: AsyncData> = AsyncData.Uninitialized, -) = SearchAccountProviderState( - userInput = userInput, - userInputResult = userInputResult, - changeServerState = aChangeServerState(), - eventSink = {} -) - -fun aHomeserverDataList(): List { - return listOf( - aHomeserverData(homeserverUrl = AuthenticationConfig.MATRIX_ORG_URL), - aHomeserverData(homeserverUrl = "https://no.sliding.sync"), - aHomeserverData(homeserverUrl = "https://invalid"), - ) -} - -fun aHomeserverData( - homeserverUrl: String = AuthenticationConfig.MATRIX_ORG_URL, -): HomeserverData { - return HomeserverData(homeserverUrl = homeserverUrl) -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt deleted file mode 100644 index 8335dde112..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt +++ /dev/null @@ -1,214 +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. - */ - -@file:OptIn(ExperimentalMaterial3Api::class) - -package io.element.android.features.login.impl.screens.searchaccountprovider - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusDirection -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import io.element.android.appconfig.AuthenticationConfig -import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.login.impl.R -import io.element.android.features.login.impl.accountprovider.AccountProvider -import io.element.android.features.login.impl.accountprovider.AccountProviderView -import io.element.android.features.login.impl.changeserver.ChangeServerEvents -import io.element.android.features.login.impl.changeserver.ChangeServerView -import io.element.android.features.login.impl.resolver.HomeserverData -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule -import io.element.android.libraries.designsystem.components.BigIcon -import io.element.android.libraries.designsystem.components.button.BackButton -import io.element.android.libraries.designsystem.components.form.textFieldState -import io.element.android.libraries.designsystem.modifiers.onTabOrEnterKeyFocusNext -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.TextField -import io.element.android.libraries.designsystem.theme.components.TopAppBar -import io.element.android.libraries.testtags.TestTags -import io.element.android.libraries.testtags.testTag -import io.element.android.libraries.ui.strings.CommonStrings - -/** - * https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=611-61435 - */ -@Composable -fun SearchAccountProviderView( - state: SearchAccountProviderState, - onBackClick: () -> Unit, - onLearnMoreClick: () -> Unit, - onSuccess: () -> Unit, - modifier: Modifier = Modifier, -) { - val eventSink = state.eventSink - Scaffold( - modifier = modifier, - topBar = { - TopAppBar( - title = {}, - navigationIcon = { BackButton(onClick = onBackClick) } - ) - } - ) { padding -> - Box( - modifier = Modifier - .fillMaxSize() - .imePadding() - .padding(padding) - .consumeWindowInsets(padding) - ) { - LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) { - item { - IconTitleSubtitleMolecule( - modifier = Modifier.padding(top = 16.dp, bottom = 40.dp, start = 16.dp, end = 16.dp), - iconStyle = BigIcon.Style.Default(CompoundIcons.Search()), - title = stringResource(id = R.string.screen_account_provider_form_title), - subTitle = stringResource(id = R.string.screen_account_provider_form_subtitle), - ) - } - item { - // TextInput - var userInputState by textFieldState(stateValue = state.userInput) - val focusManager = LocalFocusManager.current - TextField( - value = userInputState, - // readOnly = isLoading, - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .onTabOrEnterKeyFocusNext(focusManager) - .testTag(TestTags.changeServerServer), - onValueChange = { - userInputState = it - eventSink(SearchAccountProviderEvents.UserInput(it)) - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Uri, - imeAction = ImeAction.Done, - ), - keyboardActions = KeyboardActions(onDone = { - focusManager.moveFocus(FocusDirection.Down) - }), - singleLine = true, - trailingIcon = if (userInputState.isNotEmpty()) { - { - Box( - Modifier.clickable( - onClickLabel = stringResource(CommonStrings.action_clear), - role = Role.Button, - ) { - userInputState = "" - eventSink(SearchAccountProviderEvents.UserInput("")) - } - ) { - Icon( - imageVector = CompoundIcons.Close(), - contentDescription = stringResource(CommonStrings.action_clear) - ) - } - } - } else { - null - }, - supportingText = stringResource(id = R.string.screen_account_provider_form_notice), - ) - } - - when (state.userInputResult) { - is AsyncData.Failure -> { - // Ignore errors (let the user type more chars) - } - is AsyncData.Loading -> { - item { - Box( - modifier = Modifier - .fillMaxSize() - ) { - CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center) - ) - } - } - } - is AsyncData.Success -> { - items(state.userInputResult.data) { homeserverData -> - val item = homeserverData.toAccountProvider() - AccountProviderView( - item = item, - onClick = { - state.changeServerState.eventSink.invoke(ChangeServerEvents.ChangeServer(item)) - } - ) - } - } - AsyncData.Uninitialized -> Unit - } - item { - Spacer(Modifier.height(32.dp)) - } - } - ChangeServerView( - state = state.changeServerState, - onLearnMoreClick = onLearnMoreClick, - onSuccess = onSuccess, - ) - } - } -} - -@Composable -private fun HomeserverData.toAccountProvider(): AccountProvider { - val isMatrixOrg = homeserverUrl == AuthenticationConfig.MATRIX_ORG_URL - return AccountProvider( - url = homeserverUrl, - subtitle = if (isMatrixOrg) stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle) else null, - // There is no need to know for other servers right now - isPublic = isMatrixOrg, - isMatrixOrg = isMatrixOrg, - ) -} - -@PreviewsDayNight -@Composable -internal fun SearchAccountProviderViewPreview(@PreviewParameter(SearchAccountProviderStateProvider::class) state: SearchAccountProviderState) = ElementPreview { - SearchAccountProviderView( - state = state, - onBackClick = {}, - onLearnMoreClick = {}, - onSuccess = {}, - ) -} diff --git a/features/login/impl/src/main/res/values-es/translations.xml b/features/login/impl/src/main/res/values-es/translations.xml index 33b3741234..13a93164a6 100644 --- a/features/login/impl/src/main/res/values-es/translations.xml +++ b/features/login/impl/src/main/res/values-es/translations.xml @@ -25,8 +25,16 @@ "No se permite el proveedor de cuentas %1$s." "URL del servidor base" "Introduce una dirección de dominio." + "Iniciar sesión" + "Crear cuenta" "¿Cuál es la dirección de tu servidor?" + "Si tu cuenta te la proporciona tu empresa o una comunidad, cambia el proveedor de la cuenta (ejemplo: nombredeempresa.com)." + "Crea una cuenta gratuita en matrix.org. Si deseas crear una cuenta con otro proveedor, modifícala según corresponda (por ejemplo: communityname.org)." + "Proveedor de cuentas" + "ejemplo.com" "Selecciona tu servidor" + "Introduce tu proveedor de cuenta" + "Elegir proveedor de cuentas" "Crear cuenta" "Esta cuenta ha sido eliminada." "Usuario y/o contraseña incorrectos" diff --git a/features/login/impl/src/main/res/values-et/translations.xml b/features/login/impl/src/main/res/values-et/translations.xml index dc37ed3e38..a27bc18e3e 100644 --- a/features/login/impl/src/main/res/values-et/translations.xml +++ b/features/login/impl/src/main/res/values-et/translations.xml @@ -25,8 +25,16 @@ "%1$s teenusepakkuja pole lubatud." "Koduserveri url" "Sisesta domeeni aadress." + "Logi sisse" + "Loo kasutajakonto" "Mis on sinu koduserveri aadress?" + "Kui sinu kasutajakonto paikneb äriühingu või kogukonna serveris, siis muuda kontoteenuse pakkujat (näiteks: minufirmanimi.ee)." + "Loo tasuta kasutajakonto matrix.org serverisse. Kui soovid luua kasutajakonto mõne teise teenusepakkuja juures, siis muuda seda vastavalt (näiteks: minukogukond.ee)." + "Teenusepakkuja" + "domeeninäidis.ee" "Vali oma server" + "Sisesta oma kasutajakonto teenusepakkuja" + "Vali kasutajakonto teenusepakkuja" "Loo kasutajakonto" "See kasutajakonto on kustutatud." "Vigane kasutajanimi ja/või salasõna" diff --git a/features/login/impl/src/main/res/values-fi/translations.xml b/features/login/impl/src/main/res/values-fi/translations.xml index 8561bcfce3..493ddee2f8 100644 --- a/features/login/impl/src/main/res/values-fi/translations.xml +++ b/features/login/impl/src/main/res/values-fi/translations.xml @@ -25,8 +25,16 @@ "Palveluntarjoaja %1$s ei ole sallittu." "Kotipalvelimen osoite" "Anna verkkotunnuksen osoite." + "Kirjaudu sisään" + "Luo tili" "Mikä on palvelimesi osoite?" + "Jos tilisi on yrityksesi tai yhteisön tarjoama, vaihda palveluntarjoaja (esimerkki: companyname.com)." + "Luo tili matrix.org -palvelimelle ilmaiseksi. Jos haluat luoda tilin eri palveluntarjoajan kautta, muuta sitä vastaavasti (esimerkiksi: communityname.org)." + "Palveluntarjoaja" + "example.com" "Valitse palvelimesi" + "Anna palveluntarjoajasi" + "Valitse palveluntarjoaja" "Luo tili" "Tämä tili on poistettu." "Väärä käyttäjänimi ja/tai salasana" diff --git a/features/login/impl/src/main/res/values-fr/translations.xml b/features/login/impl/src/main/res/values-fr/translations.xml index 504517aad7..0af658d34f 100644 --- a/features/login/impl/src/main/res/values-fr/translations.xml +++ b/features/login/impl/src/main/res/values-fr/translations.xml @@ -25,8 +25,16 @@ "Le fournisseur de compte %1$s n’est pas autorisé." "URL du serveur d’accueil" "Saisissez une adresse de domaine." + "Se connecter" + "Créer un compte" "Quelle est l’adresse de votre serveur ?" + "Si votre fournisseur de compte est fourni par votre entreprise ou une communauté, veuillez le saisir ici (exemple : monentreprise.com)." + "Créez un compte sur matrix.org gratuitement. Si vous souhaitez créer un compte auprès d’un autre fournisseur, veuillez modifier l’adresse ici (exemple : monassociation.org)." + "Fournisseur de compte" + "exemple.com" "Choisissez votre serveur" + "Saisissez votre fournisseur de compte" + "Choisissez un fournisseur de compte" "Créer un compte" "Ce compte a été supprimé." "Nom d’utilisateur et/ou mot de passe incorrects" diff --git a/features/login/impl/src/main/res/values-in/translations.xml b/features/login/impl/src/main/res/values-in/translations.xml index 1d29c5b619..34ba1bd0ed 100644 --- a/features/login/impl/src/main/res/values-in/translations.xml +++ b/features/login/impl/src/main/res/values-in/translations.xml @@ -25,8 +25,16 @@ "Penyedia akun %1$s tidak diizinkan." "URL Homeserver" "Masukkan alamat domain." + "Login" + "Buat akun" "Apa alamat server Anda?" + "Jika akun Anda disediakan oleh perusahaan atau komunitas Anda, maka ubah penyedia akun (contoh: companyname.com)." + "Buat akun di matrix.org secara gratis. Jika anda ingin membuat akun dengan penyedia lain, ubah sesuai kebutuhan (contoh: communityname.org)." + "Penyedia akun" + "contoh.com" "Pilih server Anda" + "Masukkan penyedia akun Anda" + "Pilih penyedia akun" "Buat akun" "Akun ini telah dinonaktifkan." "Nama pengguna dan/atau kata sandi salah" diff --git a/features/login/impl/src/main/res/values-ja/translations.xml b/features/login/impl/src/main/res/values-ja/translations.xml index 42698e27dc..b4d6f16721 100644 --- a/features/login/impl/src/main/res/values-ja/translations.xml +++ b/features/login/impl/src/main/res/values-ja/translations.xml @@ -24,8 +24,16 @@ "アカウント提供元 %1$s は許可されていません。" "ホームサーバーURL" "ドメイン名を入力してください" + "サインイン" + "アカウントを作成" "サーバーのアドレスは何ですか?" + "アカウントが会社やコミュニティから発行されている場合は、アカウント提供元を変更してください (例: kaisha-mei.com)。" + "matrix.org 上に無料でアカウントを作成できます。異なるアカウント提供元上にアカウントを作成したい場合は、それにならって変更してください (例: dantai-mei.org)。" + "アカウント提供元" + "example.com" "サーバーを選択" + "アカウント提供元を入力" + "アカウント提供元を選択" "アカウントを作成" "アカウントは削除されました。" "ユーザー名またはパスワードが違います" diff --git a/features/login/impl/src/main/res/values-pl/translations.xml b/features/login/impl/src/main/res/values-pl/translations.xml index 634d856cdf..81d8570e32 100644 --- a/features/login/impl/src/main/res/values-pl/translations.xml +++ b/features/login/impl/src/main/res/values-pl/translations.xml @@ -25,8 +25,16 @@ "Dostawca konta %1$s jest niedozwolony." "URL serwera domowego" "Wprowadź adres domeny." + "Zaloguj" + "Utwórz konto" "Jaki jest adres Twojego serwera?" + "Jeśli uzyskałeś konto od firmy lub społeczności, wprowadź dostawcę konta (na przykład: nazwafirmy.com)" + "Załóż darmowe konto na matrix.org. Jeśli chcesz założyć konto u innego dostawcy wprowadź własny adres (na przykład: nazwaspołeczności.org)." + "Dostawca konta" + "przykład.com" "Wybierz swój serwer" + "Wprowadź dostawcę swojego konta" + "Wybierz dostawcę konta" "Utwórz konto" "To konto zostało usunięte." "Nieprawidłowa nazwa użytkownika i/lub hasło" diff --git a/features/login/impl/src/main/res/values-ro/translations.xml b/features/login/impl/src/main/res/values-ro/translations.xml index b44242abf8..6835c0a15d 100644 --- a/features/login/impl/src/main/res/values-ro/translations.xml +++ b/features/login/impl/src/main/res/values-ro/translations.xml @@ -25,8 +25,16 @@ "Furnizorul de cont %1$s nu este permis." "Adresa URL a homeserver-ului" "Introduceți o adresă de domeniu." + "Autentificare" + "Creați un cont" "Care este adresa serverului dumneavoastră?" + "Dacă contul este furnizat de compania dumneavoastră sau de o comunitate, schimbați furnizorul contului (exemplu: numecompanie.com)." + "Creați un cont gratuit pe matrix.org. Dacă doriți să creați un cont la un alt furnizor, schimbați-l în consecință (exemplu: numecomunitate.org)." + "Furnizor de cont" + "exemplu.ro" "Selectați serverul dumneavoastra" + "Introduceți furnizorul contului dumneavoastră" + "Alegeți un furnizor de cont" "Creați un cont" "Acest cont a fost șters." "Utilizator și/sau parolă incorecte" diff --git a/features/login/impl/src/main/res/values-uk/translations.xml b/features/login/impl/src/main/res/values-uk/translations.xml index 60959c80d4..918caf5530 100644 --- a/features/login/impl/src/main/res/values-uk/translations.xml +++ b/features/login/impl/src/main/res/values-uk/translations.xml @@ -25,8 +25,16 @@ "Постачальник облікового запису %1$s не дозволений." "URL-адреса домашнього сервера" "Введіть адресу домену." + "Увійти" + "Створити обліковий запис" "Яка адреса вашого сервера?" + "Якщо ваш обліковий запис надано вашою компанією або спільнотою, змініть постачальника облікового запису (наприклад: companyname.com)." + "Створіть обліковий запис на matrix.org безплатно. Якщо ви хочете створити обліковий запис у іншого постачальника, змініть його відповідно (наприклад: communityname.org)." + "Постачальник облікового запису" + "example.com" "Виберіть свій сервер" + "Вкажіть постачальника вашого облікового запису" + "Оберіть постачальника облікового запису" "Створити обліковий запис" "Цей обліковий запис було видалено." "Неправильне ім\'я користувача та/або пароль" diff --git a/features/login/impl/src/main/res/values-zh/translations.xml b/features/login/impl/src/main/res/values-zh/translations.xml index 16afe7d5bd..85de302dc1 100644 --- a/features/login/impl/src/main/res/values-zh/translations.xml +++ b/features/login/impl/src/main/res/values-zh/translations.xml @@ -25,8 +25,16 @@ "账户提供者 %1$s 不被允许。" "主服务器 URL" "输入域名地址。" + "登录" + "创建账户" "你的服务器地址是什么?" + "如果你的账户由你的公司或社区提供,则应更改账户提供者(例如:companyname.com)。" + "在 matrix.org 创建免费账户。如果你想使用其它服务提供者创建账户,请相应地更改域名(例如:communityname.org)。" + "账户提供者" + "example.com" "选择服务器" + "输入你的账户提供者" + "选择账户提供者" "创建账户" "此账户已被删除。" "用户名与(或)密码不正确" diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index 10fb6ef04a..7e798081f5 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -25,8 +25,16 @@ "Account provider %1$s not allowed." "Homeserver URL" "Enter a domain address." + "Sign in" + "Create account" "What is the address of your server?" + "If your account is provided by your company or a community, then change the account provider (example: companyname.com)." + "Create an account on matrix.org for free. If you want to create an account with a different provider, then change it accordingly (example: communityname.org)." + "Account provider" + "example.com" "Select your server" + "Enter your account provider" + "Choose an account provider" "Create account" "This account has been deleted." "Incorrect username and/or password" diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPointTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPointTest.kt index a05194d008..8a0bf4c307 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPointTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPointTest.kt @@ -12,12 +12,11 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.testing.junit4.util.MainDispatcherRule import com.google.common.truth.Truth.assertThat -import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.api.LoginEntryPoint -import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.accountprovider.anAccountProviderDataSource import io.element.android.features.login.impl.classic.FakeElementClassicConnection import io.element.android.features.preferences.test.FakePreferencesEntryPoint -import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow +import io.element.android.libraries.oauth.test.FakeOAuthActionFlow import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.node.TestParentNode import kotlinx.coroutines.test.runTest @@ -38,7 +37,7 @@ class DefaultLoginEntryPointTest { LoginFlowNode( buildContext = buildContext, plugins = plugins, - accountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), + accountProviderDataSource = anAccountProviderDataSource(), oAuthActionFlow = FakeOAuthActionFlow(), appCoroutineScope = backgroundScope, elementClassicConnection = FakeElementClassicConnection(), diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSourceFixture.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSourceFixture.kt new file mode 100644 index 0000000000..25bc67144b --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSourceFixture.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.login.impl.accountprovider + +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.features.enterprise.test.FakeEnterpriseService +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers + +fun anAccountProviderDataSource( + enterpriseService: EnterpriseService = FakeEnterpriseService(), + appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), + coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Unconfined), +) = AccountProviderDataSource( + enterpriseService = enterpriseService, + appPreferencesStore = appPreferencesStore, + coroutineScope = coroutineScope, +) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSourceTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSourceTest.kt index f86df13335..30e25c8c3e 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSourceTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSourceTest.kt @@ -13,6 +13,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.tests.testutils.WarmUpRule import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -24,7 +25,7 @@ class AccountProviderDataSourceTest { @Test fun `present - initial state`() = runTest { - val sut = AccountProviderDataSource(FakeEnterpriseService()) + val sut = anAccountProviderDataSource() sut.flow.test { val initialState = awaitItem() assertThat(initialState).isEqualTo( @@ -41,10 +42,10 @@ class AccountProviderDataSourceTest { @Test fun `present - initial state - matrix org`() = runTest { - val sut = AccountProviderDataSource( - FakeEnterpriseService( + val sut = anAccountProviderDataSource( + enterpriseService = FakeEnterpriseService( defaultHomeserverListResult = { listOf(AuthenticationConfig.MATRIX_ORG_URL) } - ) + ), ) sut.flow.test { val initialState = awaitItem() @@ -62,10 +63,10 @@ class AccountProviderDataSourceTest { @Test fun `present - ensure that default homeserver is not star char`() = runTest { - val sut = AccountProviderDataSource( - FakeEnterpriseService( + val sut = anAccountProviderDataSource( + enterpriseService = FakeEnterpriseService( defaultHomeserverListResult = { listOf(EnterpriseService.ANY_ACCOUNT_PROVIDER, AuthenticationConfig.MATRIX_ORG_URL) } - ) + ), ) sut.flow.test { val initialState = awaitItem() @@ -83,7 +84,7 @@ class AccountProviderDataSourceTest { @Test fun `present - user change and reset`() = runTest { - val sut = AccountProviderDataSource(FakeEnterpriseService()) + val sut = anAccountProviderDataSource() sut.flow.test { val initialState = awaitItem() assertThat(initialState.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL) @@ -106,7 +107,7 @@ class AccountProviderDataSourceTest { @Test fun `present - set url and reset`() = runTest { - val sut = AccountProviderDataSource(FakeEnterpriseService()) + val sut = anAccountProviderDataSource() sut.flow.test { val initialState = awaitItem() assertThat(initialState.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL) @@ -126,4 +127,47 @@ class AccountProviderDataSourceTest { assertThat(resetState.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL) } } + + @Test + fun `present - defaults to the most recently used provider from history`() = runTest { + val sut = anAccountProviderDataSource( + appPreferencesStore = InMemoryAppPreferencesStore( + homeserverHistory = listOf("https://example.com", "https://matrix.org"), + ), + ) + sut.flow.test { + assertThat(awaitItem().url).isEqualTo("https://example.com") + } + } + + @Test + fun `present - history is ignored when the account provider is enforced`() = runTest { + val sut = anAccountProviderDataSource( + enterpriseService = FakeEnterpriseService( + defaultHomeserverListResult = { listOf("https://enforced.org") } + ), + appPreferencesStore = InMemoryAppPreferencesStore( + homeserverHistory = listOf("https://example.com"), + ), + ) + sut.flow.test { + assertThat(awaitItem().url).isEqualTo("https://enforced.org") + } + } + + @Test + fun `present - reset returns to the most recently used provider from history`() = runTest { + val sut = anAccountProviderDataSource( + appPreferencesStore = InMemoryAppPreferencesStore( + homeserverHistory = listOf("https://example.com"), + ), + ) + sut.flow.test { + assertThat(awaitItem().url).isEqualTo("https://example.com") + sut.setUrl("https://other.com") + assertThat(awaitItem().url).isEqualTo("https://other.com") + sut.reset() + assertThat(awaitItem().url).isEqualTo("https://example.com") + } + } } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/SaveAccountProviderToHistoryTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/SaveAccountProviderToHistoryTest.kt new file mode 100644 index 0000000000..0af8188f3f --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/SaveAccountProviderToHistoryTest.kt @@ -0,0 +1,50 @@ +/* + * 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.login.impl.accountprovider + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore +import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class SaveAccountProviderToHistoryTest { + @get:Rule + val warmUpRule = WarmUpRule() + + @Test + fun `invoke saves the current account provider url to history`() = runTest { + val appPreferencesStore = InMemoryAppPreferencesStore() + val accountProviderDataSource = anAccountProviderDataSource() + accountProviderDataSource.setUrl("https://example.com") + val sut = SaveAccountProviderToHistory(accountProviderDataSource, appPreferencesStore) + + sut() + + assertThat(appPreferencesStore.getHomeserverHistoryFlow().first()) + .containsExactly("https://example.com") + } + + @Test + fun `invoke saves the selected provider even after the exposed default has been reset`() = runTest { + val appPreferencesStore = InMemoryAppPreferencesStore() + val accountProviderDataSource = anAccountProviderDataSource(appPreferencesStore = appPreferencesStore) + accountProviderDataSource.setUrl("https://example.com") + // Simulate the login flow being torn down (e.g. across an OAuth round-trip), which reverts the + // exposed default; the user's selection must still be the one saved to history. + accountProviderDataSource.reset() + val sut = SaveAccountProviderToHistory(accountProviderDataSource, appPreferencesStore) + + sut() + + assertThat(appPreferencesStore.getHomeserverHistoryFlow().first()) + .containsExactly("https://example.com") + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt index 71663c5e7f..44a3bbd5cb 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt @@ -14,6 +14,7 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.accountprovider.anAccountProviderDataSource import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate import io.element.android.libraries.architecture.AsyncData @@ -62,11 +63,11 @@ class ChangeServerPresenterTest { ).test { val initialState = awaitItem() assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) - initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) + initialState.eventSink.invoke(ChangeServerEvent.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) val loadingState = awaitItem() assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() - assertThat(successState.changeServerAction).isEqualTo(AsyncData.Success(Unit)) + assertThat(successState.changeServerAction).isEqualTo(AsyncData.Success(aMatrixHomeServerDetails(supportsOAuthLogin = true))) } } @@ -86,13 +87,13 @@ class ChangeServerPresenterTest { ).test { val initialState = awaitItem() assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) - initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) + initialState.eventSink.invoke(ChangeServerEvent.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) val loadingState = awaitItem() assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java) val failureState = awaitItem() assertThat(failureState.changeServerAction).isInstanceOf(AsyncData.Failure::class.java) // Clear error - failureState.eventSink.invoke(ChangeServerEvents.ClearError) + failureState.eventSink.invoke(ChangeServerEvent.ClearError) val finalState = awaitItem() assertThat(finalState.changeServerAction).isEqualTo(AsyncData.Uninitialized) } @@ -114,7 +115,7 @@ class ChangeServerPresenterTest { ).test { val initialState = awaitItem() assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) - initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) + initialState.eventSink.invoke(ChangeServerEvent.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) val loadingState = awaitItem() assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java) val failureState = awaitItem() @@ -138,7 +139,7 @@ class ChangeServerPresenterTest { val initialState = awaitItem() assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) val anAccountProvider = AccountProvider(url = A_HOMESERVER_URL) - initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(anAccountProvider)) + initialState.eventSink.invoke(ChangeServerEvent.ChangeServer(anAccountProvider)) val loadingState = awaitItem() assertThat(loadingState.changeServerAction).isInstanceOf(AsyncData.Loading::class.java) val failureState = awaitItem() @@ -166,7 +167,7 @@ class ChangeServerPresenterTest { val initialState = awaitItem() assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) val anAccountProvider = AccountProvider(url = A_HOMESERVER_URL) - initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(anAccountProvider)) + initialState.eventSink.invoke(ChangeServerEvent.ChangeServer(anAccountProvider)) // Skip loading state skipItems(1) val failureState = awaitItem() @@ -193,7 +194,7 @@ class ChangeServerPresenterTest { ).test { val initialState = awaitItem() assertThat(initialState.localNetworkPermissionDialog).isEqualTo(LocalNetworkPermissionDialog.None) - initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) + initialState.eventSink.invoke(ChangeServerEvent.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) val promptState = expectMostRecentItem() // Dialog is shown, permission has not been requested yet, setHomeserver has not been called. assertThat(promptState.localNetworkPermissionDialog).isNotEqualTo(LocalNetworkPermissionDialog.None) @@ -212,10 +213,10 @@ class ChangeServerPresenterTest { permissionsPresenter = FakePermissionsPresenter(), ).test { val initialState = awaitItem() - initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) + initialState.eventSink.invoke(ChangeServerEvent.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) val promptState = expectMostRecentItem() assertThat(promptState.localNetworkPermissionDialog).isNotEqualTo(LocalNetworkPermissionDialog.None) - promptState.eventSink.invoke(ChangeServerEvents.DismissLocalNetworkPermission) + promptState.eventSink.invoke(ChangeServerEvent.DismissLocalNetworkPermission) val dismissedState = expectMostRecentItem() assertThat(dismissedState.localNetworkPermissionDialog).isEqualTo(LocalNetworkPermissionDialog.None) assertThat(dismissedState.changeServerAction).isEqualTo(AsyncData.Uninitialized) @@ -235,7 +236,7 @@ class ChangeServerPresenterTest { permissionsPresenter = permissionsPresenter, ).test { val initialState = awaitItem() - initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) + initialState.eventSink.invoke(ChangeServerEvent.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) val promptState = expectMostRecentItem() assertThat(promptState.localNetworkPermissionDialog).isNotEqualTo(LocalNetworkPermissionDialog.None) permissionsPresenter.setPermissionGranted() @@ -244,13 +245,13 @@ class ChangeServerPresenterTest { while (finalState.changeServerAction !is AsyncData.Success) { finalState = awaitItem() } - assertThat(finalState.changeServerAction).isEqualTo(AsyncData.Success(Unit)) + assertThat(finalState.changeServerAction).isEqualTo(AsyncData.Success(aMatrixHomeServerDetails(supportsOAuthLogin = true))) } } private fun createPresenter( authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(), - accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), + accountProviderDataSource: AccountProviderDataSource = anAccountProviderDataSource(), enterpriseService: EnterpriseService = FakeEnterpriseService(), localNetworkPermissionAdvisor: FakeLocalNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(), permissionsPresenter: FakePermissionsPresenter = FakePermissionsPresenter(), diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolverTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolverTest.kt deleted file mode 100644 index 38e04b6e7f..0000000000 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolverTest.kt +++ /dev/null @@ -1,108 +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.login.impl.resolver - -import app.cash.turbine.test -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.test.auth.FakeHomeServerLoginCompatibilityChecker -import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor -import io.element.android.tests.testutils.lambda.lambdaRecorder -import io.element.android.tests.testutils.lambda.value -import io.element.android.tests.testutils.testCoroutineDispatchers -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class HomeserverResolverTest { - @Test - fun `resolve - a server name is used as is`() = runTest { - val checkResult = lambdaRecorder> { Result.success(it == "https://example.org") } - val sut = createHomeserverResolver(checkResult) - sut.resolve("example.org").test { - assertThat(awaitItem()).isEqualTo(listOf(HomeserverData(homeserverUrl = "https://example.org"))) - awaitComplete() - } - checkResult.assertions().isCalledExactly(1) - .withSequence( - listOf(value("https://example.org")), - ) - } - - @Test - fun `resolve - an url is used as is`() = runTest { - val checkResult = lambdaRecorder> { Result.success(it == "https://example.org") } - val sut = createHomeserverResolver(checkResult) - sut.resolve("https://example.org").test { - assertThat(awaitItem()).isEqualTo(listOf(HomeserverData(homeserverUrl = "https://example.org"))) - awaitComplete() - } - checkResult.assertions().isCalledExactly(1) - .withSequence( - listOf(value("https://example.org")), - ) - } - - @Test - fun `resolve - a user id resolves the server name of the user id`() = runTest { - val checkResult = lambdaRecorder> { Result.success(it == "https://example.org") } - val sut = createHomeserverResolver(checkResult) - sut.resolve("@alice:example.org").test { - assertThat(awaitItem()).isEqualTo(listOf(HomeserverData(homeserverUrl = "https://example.org"))) - awaitComplete() - } - checkResult.assertions().isCalledExactly(1) - .withSequence( - listOf(value("https://example.org")), - ) - } - - @Test - fun `resolve - a user id with a port resolves the server name of the user id`() = runTest { - val checkResult = lambdaRecorder> { Result.success(it == "https://example.org:8448") } - val sut = createHomeserverResolver(checkResult) - sut.resolve("@alice:example.org:8448").test { - assertThat(awaitItem()).isEqualTo(listOf(HomeserverData(homeserverUrl = "https://example.org:8448"))) - awaitComplete() - } - checkResult.assertions().isCalledExactly(1) - .withSequence( - listOf(value("https://example.org:8448")), - ) - } - - @Test - fun `resolve - an incomplete user id does not resolve anything`() = runTest { - val checkResult = lambdaRecorder> { Result.success(true) } - val sut = createHomeserverResolver(checkResult) - sut.resolve("@alice").test { - awaitComplete() - } - sut.resolve("@alice:").test { - awaitComplete() - } - checkResult.assertions().isNeverCalled() - } - - @Test - fun `resolve - a user id with a too short server name does not resolve anything`() = runTest { - val checkResult = lambdaRecorder> { Result.success(true) } - val sut = createHomeserverResolver(checkResult) - sut.resolve("@alice:a.b").test { - awaitComplete() - } - checkResult.assertions().isNeverCalled() - } - - private fun TestScope.createHomeserverResolver( - checkResult: (String) -> Result, - ) = HomeserverResolver( - dispatchers = testCoroutineDispatchers(), - homeServerLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult), - localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(), - ) -} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt deleted file mode 100644 index 7d81f05c0c..0000000000 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt +++ /dev/null @@ -1,113 +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.features.login.impl.screens.changeaccountprovider - -import com.google.common.truth.Truth.assertThat -import io.element.android.features.enterprise.api.EnterpriseService -import io.element.android.features.enterprise.test.FakeEnterpriseService -import io.element.android.features.login.impl.accountprovider.AccountProvider -import io.element.android.features.login.impl.changeserver.aChangeServerState -import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER -import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2 -import io.element.android.tests.testutils.WarmUpRule -import io.element.android.tests.testutils.test -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -class ChangeAccountProviderPresenterTest { - @get:Rule - val warmUpRule = WarmUpRule() - - @Test - fun `present - initial state`() = runTest { - val presenter = ChangeAccountProviderPresenter( - changeServerPresenter = { aChangeServerState() }, - enterpriseService = FakeEnterpriseService( - defaultHomeserverListResult = { emptyList() } - ), - ) - presenter.test { - val initialState = awaitItem() - assertThat(initialState.accountProviders).isEqualTo( - listOf( - AccountProvider( - url = "https://matrix.org", - title = "matrix.org", - subtitle = null, - isPublic = true, - isMatrixOrg = true, - ) - ) - ) - assertThat(initialState.canSearchForAccountProviders).isTrue() - } - } - - @Test - fun `present - fixed list of account providers`() = runTest { - val presenter = ChangeAccountProviderPresenter( - changeServerPresenter = { aChangeServerState() }, - enterpriseService = FakeEnterpriseService( - defaultHomeserverListResult = { - listOf(AN_ACCOUNT_PROVIDER, AN_ACCOUNT_PROVIDER_2) - } - ), - ) - presenter.test { - val initialState = awaitItem() - assertThat(initialState.accountProviders).isEqualTo( - listOf( - AccountProvider( - url = "https://matrix.org", - title = "matrix.org", - subtitle = null, - isPublic = true, - isMatrixOrg = true, - ), - AccountProvider( - url = "https://element.io", - title = "element.io", - subtitle = null, - isPublic = false, - isMatrixOrg = false, - ) - ) - ) - assertThat(initialState.canSearchForAccountProviders).isFalse() - } - } - - @Test - fun `present - opened list of account providers`() = runTest { - val presenter = ChangeAccountProviderPresenter( - changeServerPresenter = { aChangeServerState() }, - enterpriseService = FakeEnterpriseService( - defaultHomeserverListResult = { - listOf(AN_ACCOUNT_PROVIDER, EnterpriseService.ANY_ACCOUNT_PROVIDER) - } - ), - ) - presenter.test { - val initialState = awaitItem() - assertThat(initialState.accountProviders).isEqualTo( - listOf( - AccountProvider( - url = "https://matrix.org", - title = "matrix.org", - subtitle = null, - isPublic = true, - isMatrixOrg = true, - ) - ) - ) - assertThat(initialState.canSearchForAccountProviders).isTrue() - } - } -} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt index 4f00bb002b..78f4293489 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt @@ -88,7 +88,7 @@ class ChooseAccountProviderPresenterTest { presenter.test { awaitItem().also { assertThat(it.selectedAccountProvider).isNull() - it.eventSink(ChooseAccountProviderEvents.Continue) + it.eventSink(ChooseAccountProviderEvent.Continue) expectNoEvents() } } @@ -112,11 +112,11 @@ class ChooseAccountProviderPresenterTest { presenter.test { awaitItem().also { assertThat(it.selectedAccountProvider).isNull() - it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider1)) + it.eventSink(ChooseAccountProviderEvent.SelectAccountProvider(accountProvider1)) } awaitItem().also { assertThat(it.selectedAccountProvider).isEqualTo(accountProvider1) - it.eventSink(ChooseAccountProviderEvents.Continue) + it.eventSink(ChooseAccountProviderEvent.Continue) skipItems(1) // Loading // Check an error was returned @@ -124,7 +124,7 @@ class ChooseAccountProviderPresenterTest { assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) // Assert the error is then cleared - submittedState.eventSink(ChooseAccountProviderEvents.ClearError) + submittedState.eventSink(ChooseAccountProviderEvent.ClearError) val clearedState = awaitItem() assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } @@ -145,15 +145,15 @@ class ChooseAccountProviderPresenterTest { presenter.test { awaitItem().also { assertThat(it.selectedAccountProvider).isNull() - it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider1)) + it.eventSink(ChooseAccountProviderEvent.SelectAccountProvider(accountProvider1)) } awaitItem().also { assertThat(it.selectedAccountProvider).isEqualTo(accountProvider1) - it.eventSink(ChooseAccountProviderEvents.Continue) + it.eventSink(ChooseAccountProviderEvent.Continue) } awaitItem().also { assertThat(it.loginModeState.loginMode.isLoading()).isTrue() - it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider2)) + it.eventSink(ChooseAccountProviderEvent.SelectAccountProvider(accountProvider2)) } expectNoEvents() } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt index cd64638563..4ef7a71966 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt @@ -35,7 +35,7 @@ import org.robolectric.annotation.Config class ChooseAccountProviderViewTest : RobolectricTest() { @Test fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { - val eventSink = EventsRecorder(expectEvents = false) + val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { setChooseAccountProviderView( state = aChooseAccountProviderState( @@ -50,7 +50,7 @@ class ChooseAccountProviderViewTest : RobolectricTest() { @Config(qualifiers = "h1024dp") @Test fun `selecting an account provider emits the the expected event`() = runAndroidComposeUiTest { - val eventSink = EventsRecorder() + val eventSink = EventsRecorder() setChooseAccountProviderView( state = aChooseAccountProviderState( accountProviders = listOf( @@ -62,12 +62,12 @@ class ChooseAccountProviderViewTest : RobolectricTest() { ), ) onNodeWithText(ChooseAccountProviderPresenterTest.accountProvider1.title).performClick() - eventSink.assertSingle(ChooseAccountProviderEvents.SelectAccountProvider(ChooseAccountProviderPresenterTest.accountProvider1)) + eventSink.assertSingle(ChooseAccountProviderEvent.SelectAccountProvider(ChooseAccountProviderPresenterTest.accountProvider1)) } @Test fun `when error is displayed - closing the dialog emits the expected event`() = runAndroidComposeUiTest { - val eventSink = EventsRecorder() + val eventSink = EventsRecorder() setChooseAccountProviderView( state = aChooseAccountProviderState( loginModeState = aLoginModeState(loginMode = AsyncData.Failure(AN_EXCEPTION)), @@ -75,7 +75,7 @@ class ChooseAccountProviderViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_ok) - eventSink.assertSingle(ChooseAccountProviderEvents.ClearError) + eventSink.assertSingle(ChooseAccountProviderEvent.ClearError) } private fun AndroidComposeUiTest.setChooseAccountProviderView( diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt index 8d56386fa4..d4a1a74d99 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt @@ -8,8 +8,8 @@ package io.element.android.features.login.impl.screens.classic.loginwithclassic import com.google.common.truth.Truth.assertThat -import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.accountprovider.anAccountProviderDataSource import io.element.android.features.login.impl.classic.ElementClassicConnection import io.element.android.features.login.impl.classic.ElementClassicConnectionState import io.element.android.features.login.impl.classic.FakeElementClassicConnection @@ -249,7 +249,7 @@ private fun createPresenter( navigator: LoginWithClassicNavigator = FakeLoginWithClassicNavigator(), loginModePresenter: LoginModePresenter = createLoginModePresenter(), elementClassicConnection: ElementClassicConnection = FakeElementClassicConnection(), - accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), + accountProviderDataSource: AccountProviderDataSource = anAccountProviderDataSource(), isEnterpriseBuild: Boolean = false, ) = LoginWithClassicPresenter( userId = userId, diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt index 52858b77af..19afbe0f28 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt @@ -8,10 +8,16 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider +import app.cash.turbine.ReceiveTurbine import com.google.common.truth.Truth.assertThat import io.element.android.appconfig.AuthenticationConfig +import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService +import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.accountprovider.anAccountProviderDataSource +import io.element.android.features.login.impl.changeserver.ChangeServerPresenter +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate import io.element.android.features.login.impl.login.LoginMode import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported import io.element.android.features.login.impl.screens.onboarding.createLoginModePresenter @@ -22,7 +28,12 @@ import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationSer import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails import io.element.android.libraries.oauth.api.OAuthAction import io.element.android.libraries.oauth.api.OAuthActionFlow -import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow +import io.element.android.libraries.oauth.test.FakeOAuthActionFlow +import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.test.FakePermissionsPresenter +import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.test import kotlinx.coroutines.test.runTest @@ -40,7 +51,7 @@ class ConfirmAccountProviderPresenterTest { val initialState = awaitItem() assertThat(initialState.isAccountCreation).isFalse() assertThat(initialState.submitEnabled).isTrue() - assertThat(initialState.accountProvider.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL) + assertThat(initialState.accountProviderInput).isEqualTo("matrix.org") assertThat(initialState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } } @@ -57,14 +68,10 @@ class ConfirmAccountProviderPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) - val loadingState = awaitItem() - assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) - val successState = awaitItem() - assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + initialState.eventSink.invoke(ConfirmAccountProviderEvent.Continue(AuthenticationConfig.MATRIX_ORG_URL)) + val successState = awaitLoginMode { it is AsyncData.Success } assertThat(successState.loginModeState.loginMode.dataOrNull()).isEqualTo(LoginMode.PasswordLogin) + cancelAndIgnoreRemainingEvents() } } @@ -80,14 +87,10 @@ class ConfirmAccountProviderPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) - val loadingState = awaitItem() - assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) - val successState = awaitItem() - assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + initialState.eventSink.invoke(ConfirmAccountProviderEvent.Continue(AuthenticationConfig.MATRIX_ORG_URL)) + val successState = awaitLoginMode { it is AsyncData.Success } assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + cancelAndIgnoreRemainingEvents() } } @@ -105,18 +108,14 @@ class ConfirmAccountProviderPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) - val loadingState = awaitItem() - assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) - val successState = awaitItem() - assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + initialState.eventSink.invoke(ConfirmAccountProviderEvent.Continue(AuthenticationConfig.MATRIX_ORG_URL)) + val successState = awaitLoginMode { it is AsyncData.Success } assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) authenticationService.givenOAuthCancelError(AN_EXCEPTION) defaultOAuthActionFlow.post(OAuthAction.GoBack()) - val cancelFailureState = awaitItem() + val cancelFailureState = awaitLoginMode { it is AsyncData.Failure } assertThat(cancelFailureState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + cancelAndIgnoreRemainingEvents() } } @@ -134,17 +133,13 @@ class ConfirmAccountProviderPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) - val loadingState = awaitItem() - assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) - val successState = awaitItem() - assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + initialState.eventSink.invoke(ConfirmAccountProviderEvent.Continue(AuthenticationConfig.MATRIX_ORG_URL)) + val successState = awaitLoginMode { it is AsyncData.Success } assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) defaultOAuthActionFlow.post(OAuthAction.GoBack()) - val cancelFinalState = awaitItem() + val cancelFinalState = awaitLoginMode { it is AsyncData.Uninitialized } assertThat(cancelFinalState.loginModeState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) + cancelAndIgnoreRemainingEvents() } } @@ -162,13 +157,12 @@ class ConfirmAccountProviderPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) - val loadingState = awaitItem() - assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + initialState.eventSink.invoke(ConfirmAccountProviderEvent.Continue(AuthenticationConfig.MATRIX_ORG_URL)) + awaitLoginMode { it is AsyncData.Loading } defaultOAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true)) - val cancelFinalState = awaitItem() + val cancelFinalState = awaitLoginMode { it is AsyncData.Uninitialized } assertThat(cancelFinalState.loginModeState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) + cancelAndIgnoreRemainingEvents() } } @@ -186,20 +180,14 @@ class ConfirmAccountProviderPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) - val loadingState = awaitItem() - assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) - val successState = awaitItem() - assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + initialState.eventSink.invoke(ConfirmAccountProviderEvent.Continue(AuthenticationConfig.MATRIX_ORG_URL)) + val successState = awaitLoginMode { it is AsyncData.Success } assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) authenticationService.givenLoginError(AN_EXCEPTION) defaultOAuthActionFlow.post(OAuthAction.Success("aUrl")) - val cancelLoadingState = awaitItem() - assertThat(cancelLoadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) - val cancelFailureState = awaitItem() + val cancelFailureState = awaitLoginMode { it is AsyncData.Failure } assertThat(cancelFailureState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + cancelAndIgnoreRemainingEvents() } } @@ -217,17 +205,13 @@ class ConfirmAccountProviderPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) - val loadingState = awaitItem() - assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) - val successState = awaitItem() - assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + initialState.eventSink.invoke(ConfirmAccountProviderEvent.Continue(AuthenticationConfig.MATRIX_ORG_URL)) + val successState = awaitLoginMode { it is AsyncData.Success } assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) defaultOidcActionFlow.post(OAuthAction.Success("aUrl")) - val successSuccessState = awaitItem() + val successSuccessState = awaitLoginMode { it is AsyncData.Loading } assertThat(successSuccessState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + cancelAndIgnoreRemainingEvents() } } @@ -243,11 +227,11 @@ class ConfirmAccountProviderPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) - skipItems(1) // Loading - val failureState = awaitItem() - assertThat(failureState.submitEnabled).isFalse() - assertThat(failureState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + initialState.eventSink.invoke(ConfirmAccountProviderEvent.Continue(AuthenticationConfig.MATRIX_ORG_URL)) + // The account provider validation fails, so the login is never attempted. + val failureState = awaitState { it.changeServerState.changeServerAction is AsyncData.Failure } + assertThat(failureState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) + cancelAndIgnoreRemainingEvents() } } @@ -264,18 +248,15 @@ class ConfirmAccountProviderPresenterTest { presenter.test { val initialState = awaitItem() - // Submit will return an error - initialState.eventSink(ConfirmAccountProviderEvents.Continue) - - skipItems(1) // Loading + // Submit will return an error while validating the account provider + initialState.eventSink(ConfirmAccountProviderEvent.Continue(AuthenticationConfig.MATRIX_ORG_URL)) // Check an error was returned - val submittedState = awaitItem() - assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + val submittedState = awaitState { it.changeServerState.changeServerAction is AsyncData.Failure } // Assert the error is then cleared - submittedState.eventSink(ConfirmAccountProviderEvents.ClearError) - val clearedState = awaitItem() + submittedState.eventSink(ConfirmAccountProviderEvent.ClearError) + val clearedState = awaitState { it.changeServerState.changeServerAction is AsyncData.Uninitialized } assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } } @@ -284,7 +265,7 @@ class ConfirmAccountProviderPresenterTest { fun `present - confirm account creation without oidc generates an error`() = runTest { val authenticationService = FakeMatrixAuthenticationService( setHomeserverResult = { - Result.success(aMatrixHomeServerDetails()) + Result.success(aMatrixHomeServerDetails(supportsPasswordLogin = true)) }, ) val presenter = createConfirmAccountProviderPresenter( @@ -293,15 +274,15 @@ class ConfirmAccountProviderPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink(ConfirmAccountProviderEvents.Continue) - skipItems(1) // Loading + initialState.eventSink(ConfirmAccountProviderEvent.Continue(AuthenticationConfig.MATRIX_ORG_URL)) // Check an error was returned - val submittedState = awaitItem() + val submittedState = awaitLoginMode { it is AsyncData.Failure } assertThat(submittedState.loginModeState.loginMode.errorOrNull()).isInstanceOf(AccountCreationNotSupported::class.java) // Assert the error is then cleared - submittedState.eventSink(ConfirmAccountProviderEvents.ClearError) - val clearedState = awaitItem() + submittedState.eventSink(ConfirmAccountProviderEvent.ClearError) + val clearedState = awaitLoginMode { it is AsyncData.Uninitialized } assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) + cancelAndIgnoreRemainingEvents() } } @@ -318,25 +299,286 @@ class ConfirmAccountProviderPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink(ConfirmAccountProviderEvents.Continue) - skipItems(1) // Loading - val submittedState = awaitItem() - assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + initialState.eventSink(ConfirmAccountProviderEvent.Continue(AuthenticationConfig.MATRIX_ORG_URL)) + val submittedState = awaitLoginMode { it is AsyncData.Success } assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - offers an autocomplete suggestion from the account provider history`() = runTest { + val presenter = createConfirmAccountProviderPresenter( + appPreferencesStore = InMemoryAppPreferencesStore( + homeserverHistory = listOf("https://randomcommunity.org"), + ), + ) + presenter.test { + val initialState = awaitItem() + assertThat(initialState.accountProviderSuggestion).isNull() + initialState.eventSink(ConfirmAccountProviderEvent.UserInputChanged("random")) + val suggestionState = awaitState { it.accountProviderInput == "random" } + // Completion is offered without the https:// scheme, even though history stores the full URL. + assertThat(suggestionState.accountProviderSuggestion).isEqualTo("randomcommunity.org") + } + } + + @Test + fun `present - continue accepts the autocomplete suggestion rather than the typed prefix`() = runTest { + val submittedUrls = mutableListOf() + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { url -> + submittedUrls.add(url) + Result.success(aMatrixHomeServerDetails(supportsPasswordLogin = true)) + }, + ) + val presenter = createConfirmAccountProviderPresenter( + matrixAuthenticationService = authenticationService, + appPreferencesStore = InMemoryAppPreferencesStore( + homeserverHistory = listOf("https://randomcommunity.org"), + ), + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvent.UserInputChanged("random")) + val suggestionState = awaitState { it.accountProviderSuggestion != null } + suggestionState.eventSink(ConfirmAccountProviderEvent.Continue("randomcommunity.org")) + awaitLoginMode { it is AsyncData.Success } + assertThat(submittedUrls.first()).isEqualTo("https://randomcommunity.org") + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - continue applies the completed account provider back into the field`() = runTest { + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { Result.success(aMatrixHomeServerDetails(supportsPasswordLogin = true)) }, + ) + val presenter = createConfirmAccountProviderPresenter( + matrixAuthenticationService = authenticationService, + appPreferencesStore = InMemoryAppPreferencesStore(homeserverHistory = listOf("https://randomcommunity.org")), + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvent.UserInputChanged("random")) + val suggestionState = awaitState { it.accountProviderSuggestion != null } + suggestionState.eventSink(ConfirmAccountProviderEvent.Continue("randomcommunity.org")) + // The field now renders the full accepted server rather than the typed prefix. + val appliedState = awaitState { it.accountProviderInput == "randomcommunity.org" } + assertThat(appliedState.accountProviderInput).isEqualTo("randomcommunity.org") + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - offers the enterprise allow-list servers as autocomplete suggestions`() = runTest { + val presenter = createConfirmAccountProviderPresenter( + appPreferencesStore = InMemoryAppPreferencesStore(homeserverHistory = emptyList()), + enterpriseService = FakeEnterpriseService( + // Preferred servers plus the "*" (any) wildcard: the wildcard is filtered out, the rest complete. + defaultHomeserverListResult = { listOf("https://element.io", EnterpriseService.ANY_ACCOUNT_PROVIDER) }, + ), + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvent.UserInputChanged("ele")) + val suggestionState = awaitState { it.accountProviderInput == "ele" } + assertThat(suggestionState.accountProviderSuggestion).isEqualTo("element.io") + } + } + + @Test + fun `present - offers matrix_org without the scheme as an autocomplete suggestion`() = runTest { + val presenter = createConfirmAccountProviderPresenter( + appPreferencesStore = InMemoryAppPreferencesStore(homeserverHistory = emptyList()), + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvent.UserInputChanged("matr")) + val suggestionState = awaitState { it.accountProviderInput == "matr" } + assertThat(suggestionState.accountProviderSuggestion).isEqualTo("matrix.org") + } + } + + @Test + fun `present - does not autocomplete a scheme-prefixed input`() = runTest { + // Candidates are bare hosts, so a scheme-prefixed input never completes them - the user completes by + // typing the bare host. This also avoids ever suggesting an insecure http scheme. + val presenter = createConfirmAccountProviderPresenter( + appPreferencesStore = InMemoryAppPreferencesStore(homeserverHistory = emptyList()), + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvent.UserInputChanged("https://mat")) + assertThat(awaitState { it.accountProviderInput == "https://mat" }.accountProviderSuggestion).isNull() + initialState.eventSink(ConfirmAccountProviderEvent.UserInputChanged("http://mat")) + assertThat(awaitState { it.accountProviderInput == "http://mat" }.accountProviderSuggestion).isNull() + } + } + + @Test + fun `present - continue prepends https to a bare account provider`() = runTest { + val submittedUrls = mutableListOf() + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { url -> + submittedUrls.add(url) + Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) + }, + ) + val presenter = createConfirmAccountProviderPresenter( + matrixAuthenticationService = authenticationService, + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvent.Continue("matrix.org")) + awaitLoginMode { it is AsyncData.Success } + assertThat(submittedUrls.first()).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - continue configures the homeserver only once`() = runTest { + // The consolidated Continue flow validates the account provider and then signs in. Login reuses the + // details resolved during validation, so setHomeserver runs exactly once rather than once per phase. + val submittedUrls = mutableListOf() + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { url -> + submittedUrls.add(url) + Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) + }, + ) + val presenter = createConfirmAccountProviderPresenter( + matrixAuthenticationService = authenticationService, + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvent.Continue(AuthenticationConfig.MATRIX_ORG_URL)) + awaitLoginMode { it is AsyncData.Success } + assertThat(submittedUrls).containsExactly(AuthenticationConfig.MATRIX_ORG_URL) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - continue with a full matrix id signs in to its homeserver and passes a login hint`() = runTest { + val submittedUrls = mutableListOf() + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { url -> + submittedUrls.add(url) + Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) + }, + ) + val presenter = createConfirmAccountProviderPresenter(matrixAuthenticationService = authenticationService) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvent.Continue("@alice:example.org")) + awaitLoginMode { it is AsyncData.Success } + assertThat(submittedUrls.first()).isEqualTo("https://example.org") + assertThat(authenticationService.getOAuthUrlLoginHint).isEqualTo("mxid:@alice:example.org") + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - continue with a matrix id keeps the port in the homeserver`() = runTest { + val submittedUrls = mutableListOf() + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { url -> + submittedUrls.add(url) + Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) + }, + ) + val presenter = createConfirmAccountProviderPresenter(matrixAuthenticationService = authenticationService) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvent.Continue("@alice:example.org:8448")) + awaitLoginMode { it is AsyncData.Success } + assertThat(submittedUrls.first()).isEqualTo("https://example.org:8448") + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - continue with a plain account provider passes no login hint`() = runTest { + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) }, + ) + val presenter = createConfirmAccountProviderPresenter(matrixAuthenticationService = authenticationService) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvent.Continue("matrix.org")) + awaitLoginMode { it is AsyncData.Success } + assertThat(authenticationService.getOAuthUrlLoginHint).isNull() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - continue with a matrix id during account creation passes no login hint`() = runTest { + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) }, + ) + val presenter = createConfirmAccountProviderPresenter( + params = ConfirmAccountProviderPresenter.Params(isAccountCreation = true), + matrixAuthenticationService = authenticationService, + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(ConfirmAccountProviderEvent.Continue("@alice:example.org")) + awaitLoginMode { it is AsyncData.Success } + assertThat(authenticationService.getOAuthUrlLoginHint).isNull() + cancelAndIgnoreRemainingEvents() + } + } + + /** + * Awaits until the emitted state's login mode matches [predicate], skipping the intermediate + * account-provider validation states, and returns that state. + */ + private suspend fun ReceiveTurbine.awaitLoginMode( + predicate: (AsyncData) -> Boolean, + ): ConfirmAccountProviderState = awaitState { predicate(it.loginModeState.loginMode) } + + private suspend fun ReceiveTurbine.awaitState( + predicate: (ConfirmAccountProviderState) -> Boolean, + ): ConfirmAccountProviderState { + while (true) { + val item = awaitItem() + if (predicate(item)) return item } } private fun createConfirmAccountProviderPresenter( params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false), - accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), + accountProviderDataSource: AccountProviderDataSource = anAccountProviderDataSource(), matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), defaultOAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(), + appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), + enterpriseService: EnterpriseService = FakeEnterpriseService(), ) = ConfirmAccountProviderPresenter( params = params, accountProviderDataSource = accountProviderDataSource, + appPreferencesStore = appPreferencesStore, + enterpriseService = enterpriseService, loginModePresenter = createLoginModePresenter( authenticationService = matrixAuthenticationService, oAuthActionFlow = defaultOAuthActionFlow, ), + changeServerPresenter = ChangeServerPresenter( + authenticationService = matrixAuthenticationService, + accountProviderDataSource = accountProviderDataSource, + defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl( + enterpriseService = FakeEnterpriseService( + isAllowedToConnectToHomeserverResult = { true }, + isElementProEnforcedResult = { false }, + ), + isEnterpriseBuild = { false }, + ), + localNetworkPermissionGate = LocalNetworkPermissionGate( + advisor = FakeLocalNetworkPermissionAdvisor(), + permissionsPresenterFactory = FakePermissionsPresenterFactory(FakePermissionsPresenter()), + ), + ), ) } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt index 31a835cb8c..b12c9ec667 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt @@ -10,8 +10,9 @@ package io.element.android.features.login.impl.screens.loginpassword import com.google.common.truth.Truth.assertThat import io.element.android.appconfig.AuthenticationConfig -import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.accountprovider.SaveAccountProviderToHistory +import io.element.android.features.login.impl.accountprovider.anAccountProviderDataSource import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.test.AN_EXCEPTION @@ -21,8 +22,11 @@ import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.A_USER_NAME_2 import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails +import io.element.android.libraries.preferences.api.store.AppPreferencesStore +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.test +import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test @@ -50,7 +54,7 @@ class LoginPasswordPresenterTest { val initialState = awaitItem() assertThat(initialState.formState.login).isEqualTo(A_USER_NAME) // Login can be changed - initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME_2)) + initialState.eventSink.invoke(LoginPasswordEvent.SetLogin(A_USER_NAME_2)) val loginChangedState = awaitItem() assertThat(loginChangedState.formState.login).isEqualTo(A_USER_NAME_2) } @@ -67,11 +71,11 @@ class LoginPasswordPresenterTest { authenticationService = authenticationService, ).test { val initialState = awaitItem() - initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) + initialState.eventSink.invoke(LoginPasswordEvent.SetLogin(A_USER_NAME)) val loginState = awaitItem() assertThat(loginState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = "")) assertThat(loginState.submitEnabled).isFalse() - initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD)) + initialState.eventSink.invoke(LoginPasswordEvent.SetPassword(A_PASSWORD)) val loginAndPasswordState = awaitItem() assertThat(loginAndPasswordState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = A_PASSWORD)) assertThat(loginAndPasswordState.submitEnabled).isTrue() @@ -89,11 +93,11 @@ class LoginPasswordPresenterTest { authenticationService = authenticationService, ).test { val initialState = awaitItem() - initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) - initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD)) + initialState.eventSink.invoke(LoginPasswordEvent.SetLogin(A_USER_NAME)) + initialState.eventSink.invoke(LoginPasswordEvent.SetPassword(A_PASSWORD)) skipItems(1) val loginAndPasswordState = awaitItem() - loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit) + loginAndPasswordState.eventSink.invoke(LoginPasswordEvent.Submit) val submitState = awaitItem() assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java) val loggedInState = awaitItem() @@ -101,6 +105,32 @@ class LoginPasswordPresenterTest { } } + @Test + fun `present - successful login saves the account provider to history`() = runTest { + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { + Result.success(aMatrixHomeServerDetails()) + }, + ) + val appPreferencesStore = InMemoryAppPreferencesStore() + createLoginPasswordPresenter( + authenticationService = authenticationService, + appPreferencesStore = appPreferencesStore, + ).test { + val initialState = awaitItem() + initialState.eventSink.invoke(LoginPasswordEvent.SetLogin(A_USER_NAME)) + initialState.eventSink.invoke(LoginPasswordEvent.SetPassword(A_PASSWORD)) + skipItems(1) + val loginAndPasswordState = awaitItem() + loginAndPasswordState.eventSink.invoke(LoginPasswordEvent.Submit) + skipItems(1) + val loggedInState = awaitItem() + assertThat(loggedInState.loginAction).isEqualTo(AsyncData.Success(A_SESSION_ID)) + assertThat(appPreferencesStore.getHomeserverHistoryFlow().first()) + .containsExactly(AuthenticationConfig.MATRIX_ORG_URL) + } + } + @Test fun `present - submit with error`() = runTest { val authenticationService = FakeMatrixAuthenticationService( @@ -112,12 +142,12 @@ class LoginPasswordPresenterTest { authenticationService = authenticationService, ).test { val initialState = awaitItem() - initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) - initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD)) + initialState.eventSink.invoke(LoginPasswordEvent.SetLogin(A_USER_NAME)) + initialState.eventSink.invoke(LoginPasswordEvent.SetPassword(A_PASSWORD)) skipItems(1) val loginAndPasswordState = awaitItem() authenticationService.givenLoginError(AN_EXCEPTION) - loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit) + loginAndPasswordState.eventSink.invoke(LoginPasswordEvent.Submit) val submitState = awaitItem() assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java) val loggedInState = awaitItem() @@ -136,19 +166,19 @@ class LoginPasswordPresenterTest { authenticationService = authenticationService, ).test { val initialState = awaitItem() - initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) - initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD)) + initialState.eventSink.invoke(LoginPasswordEvent.SetLogin(A_USER_NAME)) + initialState.eventSink.invoke(LoginPasswordEvent.SetPassword(A_PASSWORD)) skipItems(1) val loginAndPasswordState = awaitItem() authenticationService.givenLoginError(AN_EXCEPTION) - loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit) + loginAndPasswordState.eventSink.invoke(LoginPasswordEvent.Submit) val submitState = awaitItem() assertThat(submitState.loginAction).isInstanceOf(AsyncData.Loading::class.java) val loggedInState = awaitItem() // Check an error was returned assertThat(loggedInState.loginAction).isEqualTo(AsyncData.Failure(AN_EXCEPTION)) // Assert the error is then cleared - loggedInState.eventSink(LoginPasswordEvents.ClearError) + loggedInState.eventSink(LoginPasswordEvent.ClearError) val clearedState = awaitItem() assertThat(clearedState.loginAction).isEqualTo(AsyncData.Uninitialized) } @@ -157,10 +187,12 @@ class LoginPasswordPresenterTest { private fun createLoginPasswordPresenter( initialLogin: String = "", authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(), - accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), + accountProviderDataSource: AccountProviderDataSource = anAccountProviderDataSource(), + appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(), ): LoginPasswordPresenter = LoginPasswordPresenter( initialLogin = initialLogin, authenticationService = authenticationService, accountProviderDataSource = accountProviderDataSource, + saveAccountProviderToHistory = SaveAccountProviderToHistory(accountProviderDataSource, appPreferencesStore), ) } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt index 3a128f2e4f..32ce44366f 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordViewTest.kt @@ -39,7 +39,7 @@ import org.robolectric.annotation.Config class LoginPasswordViewTest : RobolectricTest() { @Test fun `clicking on back invoke back callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> setLoginPasswordView( aLoginPasswordState( @@ -53,7 +53,7 @@ class LoginPasswordViewTest : RobolectricTest() { @Test fun `changing login invokes the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setLoginPasswordView( aLoginPasswordState( eventSink = eventsRecorder, @@ -62,13 +62,13 @@ class LoginPasswordViewTest : RobolectricTest() { val userNameHint = activity!!.getString(CommonStrings.common_username) onNodeWithText(userNameHint).performTextInput(A_USER_NAME) eventsRecorder.assertSingle( - LoginPasswordEvents.SetLogin(A_USER_NAME) + LoginPasswordEvent.SetLogin(A_USER_NAME) ) } @Test fun `changing login removes new lines the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setLoginPasswordView( aLoginPasswordState( eventSink = eventsRecorder, @@ -77,13 +77,28 @@ class LoginPasswordViewTest : RobolectricTest() { val userNameHint = activity!!.getString(CommonStrings.common_username) onNodeWithText(userNameHint).performTextInput("a\nb") eventsRecorder.assertSingle( - LoginPasswordEvents.SetLogin("ab") + LoginPasswordEvent.SetLogin("ab") + ) + } + + @Test + fun `changing login removes carriage returns the expected event`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + setLoginPasswordView( + aLoginPasswordState( + eventSink = eventsRecorder, + ), + ) + val userNameHint = activity!!.getString(CommonStrings.common_username) + onNodeWithText(userNameHint).performTextInput("a\r\nb") + eventsRecorder.assertSingle( + LoginPasswordEvent.SetLogin("ab") ) } @Test fun `clearing login invokes the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(A_USER_NAME), @@ -93,13 +108,13 @@ class LoginPasswordViewTest : RobolectricTest() { val a11yClear = activity!!.getString(CommonStrings.action_clear) onNodeWithContentDescription(a11yClear).performClick() eventsRecorder.assertSingle( - LoginPasswordEvents.SetLogin("") + LoginPasswordEvent.SetLogin("") ) } @Test fun `changing password invokes the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setLoginPasswordView( aLoginPasswordState( eventSink = eventsRecorder, @@ -108,13 +123,28 @@ class LoginPasswordViewTest : RobolectricTest() { val userNameHint = activity!!.getString(CommonStrings.common_password) onNodeWithText(userNameHint).performTextInput(A_PASSWORD) eventsRecorder.assertSingle( - LoginPasswordEvents.SetPassword(A_PASSWORD) + LoginPasswordEvent.SetPassword(A_PASSWORD) + ) + } + + @Test + fun `changing password removes carriage returns the expected event`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + setLoginPasswordView( + aLoginPasswordState( + eventSink = eventsRecorder, + ), + ) + val passwordHint = activity!!.getString(CommonStrings.common_password) + onNodeWithText(passwordHint).performTextInput("a\r\nb") + eventsRecorder.assertSingle( + LoginPasswordEvent.SetPassword("ab") ) } @Test fun `reveal password makes the password visible`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(password = A_PASSWORD), @@ -135,7 +165,7 @@ class LoginPasswordViewTest : RobolectricTest() { @Test fun `when login is empty, continue button is not enabled`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(password = A_PASSWORD), @@ -148,7 +178,7 @@ class LoginPasswordViewTest : RobolectricTest() { @Test fun `when password is empty, continue button is not enabled`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(login = A_USER_NAME), @@ -162,7 +192,7 @@ class LoginPasswordViewTest : RobolectricTest() { @Config(qualifiers = "h1024dp") @Test fun `clicking on Continue sends expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setLoginPasswordView( aLoginPasswordState( formState = aLoginFormState(login = A_USER_NAME, password = A_PASSWORD), @@ -173,7 +203,7 @@ class LoginPasswordViewTest : RobolectricTest() { onNodeWithText(continueStr).assertIsEnabled() clickOn(CommonStrings.action_continue) eventsRecorder.assertSingle( - LoginPasswordEvents.Submit + LoginPasswordEvent.Submit ) } } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt index df3e3d2200..1088a87538 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt @@ -16,6 +16,8 @@ import io.element.android.features.enterprise.api.IsEnterpriseBuild import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.accountprovider.SaveAccountProviderToHistory +import io.element.android.features.login.impl.accountprovider.anAccountProviderDataSource import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate import io.element.android.features.login.impl.login.LoginModePresenter import io.element.android.libraries.architecture.AsyncData @@ -31,11 +33,12 @@ import io.element.android.libraries.matrix.test.A_LOGIN_HINT import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.oauth.api.OAuthActionFlow -import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow +import io.element.android.libraries.oauth.test.FakeOAuthActionFlow import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory +import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData @@ -150,7 +153,7 @@ class OnBoardingPresenterTest { awaitItem().also { state -> assertThat(state.canReportBug).isFalse() repeat(7) { - state.eventSink(OnBoardingEvents.OnVersionClick) + state.eventSink(OnBoardingEvent.OnVersionClick) } } expectNoEvents() @@ -165,7 +168,7 @@ class OnBoardingPresenterTest { awaitItem().also { state -> assertThat(state.canReportBug).isFalse() repeat(7) { - state.eventSink(OnBoardingEvents.OnVersionClick) + state.eventSink(OnBoardingEvent.OnVersionClick) } } assertThat(awaitItem().canReportBug).isTrue() @@ -248,7 +251,7 @@ class OnBoardingPresenterTest { Result.failure(AN_EXCEPTION) }, ) - val accountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()) + val accountProviderDataSource = anAccountProviderDataSource() val presenter = createPresenter( params = OnBoardingNode.Params( accountProvider = A_HOMESERVER_URL, @@ -269,7 +272,7 @@ class OnBoardingPresenterTest { awaitItem().also { assertThat(it.defaultAccountProvider).isEqualTo(A_HOMESERVER_URL) assertThat(accountProviderDataSource.flow.first().url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL) - it.eventSink(OnBoardingEvents.OnSignIn(A_HOMESERVER_URL_2)) + it.eventSink(OnBoardingEvent.OnSignIn(A_HOMESERVER_URL_2)) skipItems(1) // Loading // Account data source has been updated assertThat(accountProviderDataSource.flow.first().url).isEqualTo(A_HOMESERVER_URL_2) @@ -278,7 +281,7 @@ class OnBoardingPresenterTest { assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) // Assert the error is then cleared - submittedState.eventSink(OnBoardingEvents.ClearError) + submittedState.eventSink(OnBoardingEvent.ClearError) val clearedState = awaitItem() assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } @@ -299,7 +302,7 @@ private fun createPresenter( loginModePresenter: LoginModePresenter = createLoginModePresenter(), onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { null }, sessionStore: SessionStore = InMemorySessionStore(), - accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), + accountProviderDataSource: AccountProviderDataSource = anAccountProviderDataSource(), ) = OnBoardingPresenter( params = params, buildMeta = buildMeta, @@ -322,6 +325,8 @@ fun createLoginModePresenter( FakeLocalNetworkPermissionAdvisor(), permissionsPresenterFactory: PermissionsPresenter.Factory = FakePermissionsPresenterFactory(), + saveAccountProviderToHistory: SaveAccountProviderToHistory = + SaveAccountProviderToHistory(anAccountProviderDataSource(), InMemoryAppPreferencesStore()), ): LoginModePresenter = LoginModePresenter( oAuthActionFlow = oAuthActionFlow, authenticationService = authenticationService, @@ -329,4 +334,5 @@ fun createLoginModePresenter( advisor = localNetworkPermissionAdvisor, permissionsPresenterFactory = permissionsPresenterFactory, ), + saveAccountProviderToHistory = saveAccountProviderToHistory, ) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt index dfa4119711..88c9acf55f 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt @@ -39,7 +39,7 @@ import org.junit.Test class OnboardingViewTest : RobolectricTestParameter() { @Test fun `when can create account - clicking on create account calls the expected callback`() = runAndroidComposeUiTest { - val eventSink = EventsRecorder(expectEvents = false) + val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> setOnboardingView( state = anOnBoardingState( @@ -58,7 +58,7 @@ class OnboardingViewTest : RobolectricTestParameter() { @Test fun `when can go back - clicking on back calls the expected callback`() = runAndroidComposeUiTest { - val eventSink = EventsRecorder(expectEvents = false) + val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> setOnboardingView( state = anOnBoardingState( @@ -73,7 +73,7 @@ class OnboardingViewTest : RobolectricTestParameter() { @Test fun `when can login with QR code - clicking on sign in with QR code calls the expected callback`() = runAndroidComposeUiTest { - val eventSink = EventsRecorder(expectEvents = false) + val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> setOnboardingView( state = anOnBoardingState( @@ -93,7 +93,7 @@ class OnboardingViewTest : RobolectricTestParameter() { "cannot search account provider" to true, ) ) = runAndroidComposeUiTest { - val eventSink = EventsRecorder(expectEvents = false) + val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(mustChooseAccountProvider) { callback -> setOnboardingView( state = anOnBoardingState( @@ -114,7 +114,7 @@ class OnboardingViewTest : RobolectricTestParameter() { "cannot search account provider" to true, ) ) = runAndroidComposeUiTest { - val eventSink = EventsRecorder(expectEvents = false) + val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(mustChooseAccountProvider) { callback -> setOnboardingView( state = anOnBoardingState( @@ -131,7 +131,7 @@ class OnboardingViewTest : RobolectricTestParameter() { @Test fun `when sign in to pre defined account provider - clicking on button emits the expected event`() = runAndroidComposeUiTest { - val eventSink = EventsRecorder() + val eventSink = EventsRecorder() setOnboardingView( state = anOnBoardingState( defaultAccountProvider = "element.io", @@ -140,12 +140,12 @@ class OnboardingViewTest : RobolectricTestParameter() { ) val buttonText = activity!!.getString(R.string.screen_onboarding_sign_in_to, "element.io") onNodeWithText(buttonText).performClick() - eventSink.assertSingle(OnBoardingEvents.OnSignIn("element.io")) + eventSink.assertSingle(OnBoardingEvent.OnSignIn("element.io")) } @Test fun `when error is displayed - closing the dialog emits the expected event`() = runAndroidComposeUiTest { - val eventSink = EventsRecorder() + val eventSink = EventsRecorder() setOnboardingView( state = anOnBoardingState( defaultAccountProvider = "element.io", @@ -154,12 +154,12 @@ class OnboardingViewTest : RobolectricTestParameter() { ), ) clickOn(CommonStrings.action_ok) - eventSink.assertSingle(OnBoardingEvents.ClearError) + eventSink.assertSingle(OnBoardingEvent.ClearError) } @Test fun `clicking on report a problem calls the sign in callback`() = runAndroidComposeUiTest { - val eventSink = EventsRecorder(expectEvents = false) + val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> setOnboardingView( state = anOnBoardingState( @@ -176,7 +176,7 @@ class OnboardingViewTest : RobolectricTestParameter() { @Test fun `clicking on settings calls the developer settings callback`() = runAndroidComposeUiTest { - val eventSink = EventsRecorder(expectEvents = false) + val eventSink = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> setOnboardingView( state = anOnBoardingState( @@ -192,7 +192,7 @@ class OnboardingViewTest : RobolectricTestParameter() { @Test fun `cannot report a problem when the feature is disabled`() = runAndroidComposeUiTest { - val eventSink = EventsRecorder(expectEvents = false) + val eventSink = EventsRecorder(expectEvents = false) setOnboardingView( state = anOnBoardingState( canReportBug = false, @@ -205,7 +205,7 @@ class OnboardingViewTest : RobolectricTestParameter() { @Test fun `when success PasswordLogin - the expected callback is invoked and the event is received`() = runAndroidComposeUiTest { - val eventSink = EventsRecorder() + val eventSink = EventsRecorder() ensureCalledOnce { callback -> setOnboardingView( state = anOnBoardingState( @@ -215,12 +215,12 @@ class OnboardingViewTest : RobolectricTestParameter() { onNeedLoginPassword = callback, ) } - eventSink.assertSingle(OnBoardingEvents.ClearError) + eventSink.assertSingle(OnBoardingEvent.ClearError) } @Test fun `when success Oidc - the expected callback is invoked and the event is received`() = runAndroidComposeUiTest { - val eventSink = EventsRecorder() + val eventSink = EventsRecorder() val oAuthDetails = OAuthDetails("aUrl") ensureCalledOnceWithParam(oAuthDetails) { callback -> setOnboardingView( @@ -231,7 +231,7 @@ class OnboardingViewTest : RobolectricTestParameter() { onOAuthDetails = callback, ) } - eventSink.assertSingle(OnBoardingEvents.ClearError) + eventSink.assertSingle(OnBoardingEvent.ClearError) } private fun AndroidComposeUiTest.setOnboardingView( diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenterTest.kt index aabcf12e25..02bace008e 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroPresenterTest.kt @@ -37,7 +37,7 @@ class QrCodeIntroPresenterTest { val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter) val presenter = createQrCodeIntroPresenter(permissionsPresenterFactory = permissionsPresenterFactory) presenter.test { - awaitItem().eventSink(QrCodeIntroEvents.Continue) + awaitItem().eventSink(QrCodeIntroEvent.Continue) assertThat(awaitItem().canContinue).isTrue() } } @@ -48,7 +48,7 @@ class QrCodeIntroPresenterTest { val permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter) val presenter = createQrCodeIntroPresenter(permissionsPresenterFactory = permissionsPresenterFactory) presenter.test { - awaitItem().eventSink(QrCodeIntroEvents.Continue) + awaitItem().eventSink(QrCodeIntroEvent.Continue) assertThat(awaitItem().cameraPermissionState.showDialog).isTrue() } } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroViewTest.kt index 5720c68ab4..3bb600ae55 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/intro/QrCodeIntroViewTest.kt @@ -59,12 +59,12 @@ class QrCodeIntroViewTest : RobolectricTest() { @Test fun `on submit button clicked - emits the Continue event`() = runAndroidComposeUiTest { - val eventRecorder = EventsRecorder() + val eventRecorder = EventsRecorder() setQrCodeIntroView( state = aQrCodeIntroState(eventSink = eventRecorder), ) clickOn(R.string.screen_qr_code_login_initial_state_button_title) - eventRecorder.assertSingle(QrCodeIntroEvents.Continue) + eventRecorder.assertSingle(QrCodeIntroEvent.Continue) } private fun AndroidComposeUiTest.setQrCodeIntroView( diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenterTest.kt index d4f23a452c..a13590a1bc 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/qrcode/scan/QrCodeScanPresenterTest.kt @@ -50,7 +50,7 @@ class QrCodeScanPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink(QrCodeScanEvents.QrCodeScanned(byteArrayOf())) + initialState.eventSink(QrCodeScanEvent.QrCodeScanned(byteArrayOf())) assertThat(awaitItem().isScanning).isFalse() assertThat(awaitItem().authenticationAction.isLoading()).isTrue() assertThat(awaitItem().authenticationAction.isSuccess()).isTrue() @@ -65,14 +65,14 @@ class QrCodeScanPresenterTest { val presenter = createQrCodeScanPresenter(qrCodeLoginDataFactory = qrCodeLoginDataFactory) presenter.test { val initialState = awaitItem() - initialState.eventSink(QrCodeScanEvents.QrCodeScanned(byteArrayOf())) + initialState.eventSink(QrCodeScanEvent.QrCodeScanned(byteArrayOf())) assertThat(awaitItem().isScanning).isFalse() assertThat(awaitItem().authenticationAction.isLoading()).isTrue() val errorState = awaitItem() assertThat(errorState.authenticationAction.isFailure()).isTrue() - errorState.eventSink(QrCodeScanEvents.TryAgain) + errorState.eventSink(QrCodeScanEvent.TryAgain) assertThat(awaitItem().isScanning).isTrue() assertThat(awaitItem().authenticationAction.isUninitialized()).isTrue() } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt deleted file mode 100644 index 29c5d4a86c..0000000000 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt +++ /dev/null @@ -1,179 +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.features.login.impl.screens.searchaccountprovider - -import com.google.common.truth.Truth.assertThat -import io.element.android.features.login.impl.changeserver.aChangeServerState -import io.element.android.features.login.impl.resolver.HomeserverResolver -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.matrix.test.auth.FakeHomeServerLoginCompatibilityChecker -import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor -import io.element.android.tests.testutils.WarmUpRule -import io.element.android.tests.testutils.lambda.lambdaRecorder -import io.element.android.tests.testutils.lambda.value -import io.element.android.tests.testutils.test -import io.element.android.tests.testutils.testCoroutineDispatchers -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -class SearchAccountProviderPresenterTest { - @get:Rule - val warmUpRule = WarmUpRule() - - @Test - fun `present - initial state`() = runTest { - val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(true) }) - val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()), - changeServerPresenter = { aChangeServerState() } - ) - presenter.test { - val initialState = awaitItem() - assertThat(initialState.userInput).isEmpty() - assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized) - } - } - - @Test - fun `present - error while checking login compatibility`() = runTest { - val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.failure(IllegalStateException("Oops")) }) - val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()), - changeServerPresenter = { aChangeServerState() } - ) - presenter.test { - val initialState = awaitItem() - initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("https://test.org")) - val withInputState = awaitItem() - assertThat(withInputState.userInput).isEqualTo("https://test.org") - assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized) - assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java) - assertThat(awaitItem().userInputResult).isEqualTo( - AsyncData.Success( - listOf( - aHomeserverData(homeserverUrl = "https://test.org") - ) - ) - ) - } - } - - @Test - fun `present - enter text no result`() = runTest { - val fakeWellknownRetriever = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(false) }) - val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever, FakeLocalNetworkPermissionAdvisor()), - changeServerPresenter = { aChangeServerState() } - ) - presenter.test { - val initialState = awaitItem() - initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test")) - val withInputState = awaitItem() - assertThat(withInputState.userInput).isEqualTo("test") - assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized) - assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java) - assertThat(awaitItem().userInputResult).isEqualTo( - AsyncData.Success( - listOf( - aHomeserverData(homeserverUrl = "https://test"), - ) - ) - ) - } - } - - @Test - fun `present - enter text one result with wellknown`() = runTest { - val checkResult = lambdaRecorder> { - when (it) { - "https://test.org" -> Result.success(false) - "https://test.com" -> Result.success(false) - "https://test.io" -> Result.success(true) - "https://test" -> Result.success(false) - else -> error("should not happen") - } - } - val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult) - val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()), - changeServerPresenter = { aChangeServerState() } - ) - presenter.test { - val initialState = awaitItem() - initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test")) - val withInputState = awaitItem() - assertThat(withInputState.userInput).isEqualTo("test") - assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized) - assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java) - assertThat(awaitItem().userInputResult).isEqualTo( - AsyncData.Success( - listOf( - aHomeserverData(homeserverUrl = "https://test.io") - ) - ) - ) - checkResult.assertions().isCalledExactly(4) - .withSequence( - listOf(value("https://test.org")), - listOf(value("https://test.com")), - listOf(value("https://test.io")), - listOf(value("https://test")), - ) - } - } - - @Test - fun `present - enter text two results with wellknown`() = runTest { - val checkResult = lambdaRecorder> { - when (it) { - "https://test.org" -> Result.success(true) - "https://test.com" -> Result.success(false) - "https://test.io" -> Result.success(true) - "https://test" -> Result.success(false) - else -> error("should not happen") - } - } - val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult) - val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()), - changeServerPresenter = { aChangeServerState() } - ) - presenter.test { - val initialState = awaitItem() - initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test")) - val withInputState = awaitItem() - assertThat(withInputState.userInput).isEqualTo("test") - assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized) - assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java) - assertThat(awaitItem().userInputResult).isEqualTo( - AsyncData.Success( - listOf( - aHomeserverData(homeserverUrl = "https://test.org"), - ) - ) - ) - assertThat(awaitItem().userInputResult).isEqualTo( - AsyncData.Success( - listOf( - aHomeserverData(homeserverUrl = "https://test.org"), - aHomeserverData(homeserverUrl = "https://test.io"), - ) - ) - ) - checkResult.assertions().isCalledExactly(4) - .withSequence( - listOf(value("https://test.org")), - listOf(value("https://test.com")), - listOf(value("https://test.io")), - listOf(value("https://test")), - ) - } - } -} diff --git a/features/login/test/build.gradle.kts b/features/login/test/build.gradle.kts index 7073648dd2..4cbf0910db 100644 --- a/features/login/test/build.gradle.kts +++ b/features/login/test/build.gradle.kts @@ -15,6 +15,7 @@ android { } dependencies { + implementation(projects.libraries.architecture) implementation(projects.features.login.api) implementation(projects.tests.testutils) } diff --git a/features/login/test/src/main/kotlin/io/element/android/features/login/test/FakeLoginEntryPoint.kt b/features/login/test/src/main/kotlin/io/element/android/features/login/test/FakeLoginEntryPoint.kt new file mode 100644 index 0000000000..2c94144890 --- /dev/null +++ b/features/login/test/src/main/kotlin/io/element/android/features/login/test/FakeLoginEntryPoint.kt @@ -0,0 +1,24 @@ +/* + * 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.login.test + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.features.login.api.LoginEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeLoginEntryPoint( + private val createNodeResult: (BuildContext, LoginEntryPoint.Params) -> Node = { _, _ -> lambdaError() }, +) : LoginEntryPoint { + override fun createNode( + parentNode: Node, + buildContext: BuildContext, + params: LoginEntryPoint.Params, + callback: LoginEntryPoint.Callback, + ): Node = createNodeResult(buildContext, params) +} diff --git a/features/login/test/src/main/kotlin/io/element/android/features/login/test/accesscontrol/FakeAccountProviderAccessControl.kt b/features/login/test/src/main/kotlin/io/element/android/features/login/test/accesscontrol/FakeAccountProviderAccessControl.kt new file mode 100644 index 0000000000..9ebd3be7fc --- /dev/null +++ b/features/login/test/src/main/kotlin/io/element/android/features/login/test/accesscontrol/FakeAccountProviderAccessControl.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.features.login.test.accesscontrol + +import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeAccountProviderAccessControl( + private val isAllowedToConnectToAccountProviderResult: (String) -> Boolean = { lambdaError() }, +) : AccountProviderAccessControl { + override suspend fun isAllowedToConnectToAccountProvider(accountProviderUrl: String): Boolean { + return isAllowedToConnectToAccountProviderResult(accountProviderUrl) + } +} diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutEvents.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutEvent.kt similarity index 78% rename from features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutEvents.kt rename to features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutEvent.kt index cc16dfdaf1..d19a62768a 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutEvents.kt +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutEvent.kt @@ -8,7 +8,7 @@ package io.element.android.features.logout.api.direct -sealed interface DirectLogoutEvents { - data class Logout(val ignoreSdkError: Boolean) : DirectLogoutEvents - data object CloseDialogs : DirectLogoutEvents +sealed interface DirectLogoutEvent { + data class Logout(val ignoreSdkError: Boolean) : DirectLogoutEvent + data object CloseDialogs : DirectLogoutEvent } diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutState.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutState.kt index 31e0fdcd5f..75c3392ac1 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutState.kt +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutState.kt @@ -13,5 +13,5 @@ import io.element.android.libraries.architecture.AsyncAction data class DirectLogoutState( val canDoDirectSignOut: Boolean, val logoutAction: AsyncAction, - val eventSink: (DirectLogoutEvents) -> Unit, + val eventSink: (DirectLogoutEvent) -> Unit, ) diff --git a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutStateProvider.kt b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutStatePreviewParam.kt similarity index 89% rename from features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutStateProvider.kt rename to features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutStatePreviewParam.kt index feaf21ef4a..936f7d2a58 100644 --- a/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutStateProvider.kt +++ b/features/logout/api/src/main/kotlin/io/element/android/features/logout/api/direct/DirectLogoutStatePreviewParam.kt @@ -11,7 +11,7 @@ package io.element.android.features.logout.api.direct import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction -open class DirectLogoutStateProvider : PreviewParameterProvider { +open class DirectLogoutStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aDirectLogoutState(), @@ -25,7 +25,7 @@ open class DirectLogoutStateProvider : PreviewParameterProvider = AsyncAction.Uninitialized, - eventSink: (DirectLogoutEvents) -> Unit = {}, + eventSink: (DirectLogoutEvent) -> Unit = {}, ) = DirectLogoutState( canDoDirectSignOut = canDoDirectSignOut, logoutAction = logoutAction, diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutEvents.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutEvent.kt similarity index 81% rename from features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutEvents.kt rename to features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutEvent.kt index 0822444bea..e23338460c 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutEvents.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutEvent.kt @@ -8,7 +8,7 @@ package io.element.android.features.logout.impl -sealed interface LogoutEvents { - data class Logout(val ignoreSdkError: Boolean) : LogoutEvents - data object CloseDialogs : LogoutEvents +sealed interface LogoutEvent { + data class Logout(val ignoreSdkError: Boolean) : LogoutEvent + data object CloseDialogs : LogoutEvent } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt index 8176ed3d28..4ad8a6c78e 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutPresenter.kt @@ -74,16 +74,16 @@ class LogoutPresenter( } } - fun handleEvent(event: LogoutEvents) { + fun handleEvent(event: LogoutEvent) { when (event) { - is LogoutEvents.Logout -> { + is LogoutEvent.Logout -> { if (logoutAction.value.isConfirming() || event.ignoreSdkError) { localCoroutineScope.logout(logoutAction, event.ignoreSdkError) } else { logoutAction.value = AsyncAction.ConfirmingNoParams } } - LogoutEvents.CloseDialogs -> { + LogoutEvent.CloseDialogs -> { logoutAction.value = AsyncAction.Uninitialized } } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt index 9240edc61d..6233c1cb23 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutState.kt @@ -21,5 +21,5 @@ data class LogoutState( val backupUploadState: BackupUploadState, val waitingForALongTime: Boolean, val logoutAction: AsyncAction, - val eventSink: (LogoutEvents) -> Unit, + val eventSink: (LogoutEvent) -> Unit, ) diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStatePreviewParam.kt similarity index 95% rename from features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt rename to features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStatePreviewParam.kt index a3b59860b5..e2d948a7f3 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStateProvider.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutStatePreviewParam.kt @@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.api.encryption.BackupUploadState import io.element.android.libraries.matrix.api.encryption.RecoveryState import io.element.android.libraries.matrix.api.encryption.SteadyStateException -open class LogoutStateProvider : PreviewParameterProvider { +open class LogoutStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aLogoutState(), @@ -50,7 +50,7 @@ fun aLogoutState( backupUploadState: BackupUploadState = BackupUploadState.Unknown, waitingForALongTime: Boolean = false, logoutAction: AsyncAction = AsyncAction.Uninitialized, - eventSink: (LogoutEvents) -> Unit = {}, + eventSink: (LogoutEvent) -> Unit = {}, ) = LogoutState( isLastDevice = isLastDevice, backupState = backupState, diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt index 9700e54f5c..90a502fc16 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/LogoutView.kt @@ -61,7 +61,7 @@ fun LogoutView( state = state, onChangeRecoveryKeyClick = onChangeRecoveryKeyClick, onLogoutClick = { - eventSink(LogoutEvents.Logout(ignoreSdkError = false)) + eventSink(LogoutEvent.Logout(ignoreSdkError = false)) } ) }, @@ -72,13 +72,13 @@ fun LogoutView( LogoutActionDialog( state.logoutAction, onConfirmClick = { - eventSink(LogoutEvents.Logout(ignoreSdkError = false)) + eventSink(LogoutEvent.Logout(ignoreSdkError = false)) }, onForceLogoutClick = { - eventSink(LogoutEvents.Logout(ignoreSdkError = true)) + eventSink(LogoutEvent.Logout(ignoreSdkError = true)) }, onDismissDialog = { - eventSink(LogoutEvents.CloseDialogs) + eventSink(LogoutEvent.CloseDialogs) }, ) } @@ -186,7 +186,7 @@ private fun Content( @PreviewsDayNight @Composable internal fun LogoutViewPreview( - @PreviewParameter(LogoutStateProvider::class) state: LogoutState, + @PreviewParameter(LogoutStatePreviewParam::class) state: LogoutState, ) = ElementPreview { LogoutView( state, diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/di/LogoutModule.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/di/LogoutBindingContainer.kt similarity index 95% rename from features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/di/LogoutModule.kt rename to features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/di/LogoutBindingContainer.kt index 9b38fde22e..879d0fc5cf 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/di/LogoutModule.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/di/LogoutBindingContainer.kt @@ -18,7 +18,7 @@ import io.element.android.libraries.di.SessionScope @ContributesTo(SessionScope::class) @BindingContainer -interface LogoutModule { +interface LogoutBindingContainer { @Binds fun bindDirectLogoutPresenter(presenter: DirectLogoutPresenter): Presenter } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt index 7fb483a447..901e4e9654 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutView.kt @@ -11,9 +11,9 @@ package io.element.android.features.logout.impl.direct import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.PreviewParameter import dev.zacsweers.metro.ContributesBinding -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.DirectLogoutStateProvider +import io.element.android.features.logout.api.direct.DirectLogoutStatePreviewParam import io.element.android.features.logout.api.direct.DirectLogoutView import io.element.android.features.logout.impl.ui.LogoutActionDialog import io.element.android.libraries.designsystem.preview.ElementPreview @@ -28,13 +28,13 @@ class DefaultDirectLogoutView : DirectLogoutView { LogoutActionDialog( state.logoutAction, onConfirmClick = { - eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false)) + eventSink(DirectLogoutEvent.Logout(ignoreSdkError = false)) }, onForceLogoutClick = { - eventSink(DirectLogoutEvents.Logout(ignoreSdkError = true)) + eventSink(DirectLogoutEvent.Logout(ignoreSdkError = true)) }, onDismissDialog = { - eventSink(DirectLogoutEvents.CloseDialogs) + eventSink(DirectLogoutEvent.CloseDialogs) }, ) } @@ -43,7 +43,7 @@ class DefaultDirectLogoutView : DirectLogoutView { @PreviewsDayNight @Composable internal fun DefaultDirectLogoutViewPreview( - @PreviewParameter(DirectLogoutStateProvider::class) state: DirectLogoutState, + @PreviewParameter(DirectLogoutStatePreviewParam::class) state: DirectLogoutState, ) = ElementPreview { DefaultDirectLogoutView().Render(state = state) } diff --git a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenter.kt b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenter.kt index 8d8cc9141e..79798dd2ab 100644 --- a/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenter.kt +++ b/features/logout/impl/src/main/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenter.kt @@ -16,7 +16,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope 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.features.logout.impl.tools.isBackingUp import io.element.android.libraries.architecture.AsyncAction @@ -48,16 +48,16 @@ class DirectLogoutPresenter( val isLastDevice by encryptionService.isLastDevice.collectAsState() - fun handleEvent(event: DirectLogoutEvents) { + fun handleEvent(event: DirectLogoutEvent) { when (event) { - is DirectLogoutEvents.Logout -> { + is DirectLogoutEvent.Logout -> { if (logoutAction.value.isConfirming() || event.ignoreSdkError) { localCoroutineScope.logout(logoutAction, event.ignoreSdkError) } else { logoutAction.value = AsyncAction.ConfirmingNoParams } } - DirectLogoutEvents.CloseDialogs -> { + DirectLogoutEvent.CloseDialogs -> { logoutAction.value = AsyncAction.Uninitialized } } diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt index 274234a338..5b14bc5cc3 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPresenterTest.kt @@ -139,10 +139,10 @@ class LogoutPresenterTest { presenter.present() }.test { val initialState = awaitFirstItem() - initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) + initialState.eventSink.invoke(LogoutEvent.Logout(ignoreSdkError = false)) val confirmationState = awaitItem() assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams) - initialState.eventSink.invoke(LogoutEvents.CloseDialogs) + initialState.eventSink.invoke(LogoutEvent.CloseDialogs) val finalState = awaitItem() assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized) } @@ -157,10 +157,10 @@ class LogoutPresenterTest { presenter.present() }.test { val initialState = awaitFirstItem() - initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) + initialState.eventSink.invoke(LogoutEvent.Logout(ignoreSdkError = false)) val confirmationState = awaitItem() assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams) - confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) + confirmationState.eventSink.invoke(LogoutEvent.Logout(ignoreSdkError = false)) val loadingState = awaitItem() assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) val successState = awaitItem() @@ -184,15 +184,15 @@ class LogoutPresenterTest { presenter.present() }.test { val initialState = awaitFirstItem() - initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) + initialState.eventSink.invoke(LogoutEvent.Logout(ignoreSdkError = false)) val confirmationState = awaitItem() assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams) - confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) + confirmationState.eventSink.invoke(LogoutEvent.Logout(ignoreSdkError = false)) val loadingState = awaitItem() assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) val errorState = awaitItem() assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION)) - errorState.eventSink.invoke(LogoutEvents.CloseDialogs) + errorState.eventSink.invoke(LogoutEvent.CloseDialogs) val finalState = awaitItem() assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized) } @@ -214,15 +214,15 @@ class LogoutPresenterTest { presenter.present() }.test { val initialState = awaitFirstItem() - initialState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) + initialState.eventSink.invoke(LogoutEvent.Logout(ignoreSdkError = false)) val confirmationState = awaitItem() assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams) - confirmationState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = false)) + confirmationState.eventSink.invoke(LogoutEvent.Logout(ignoreSdkError = false)) val loadingState = awaitItem() assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) val errorState = awaitItem() assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION)) - errorState.eventSink.invoke(LogoutEvents.Logout(ignoreSdkError = true)) + errorState.eventSink.invoke(LogoutEvent.Logout(ignoreSdkError = true)) val loadingState2 = awaitItem() assertThat(loadingState2.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) val successState = awaitItem() diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt index 3fbae4bbc5..f69ec8f0ca 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutViewTest.kt @@ -28,20 +28,20 @@ import org.junit.Test class LogoutViewTest : RobolectricTest() { @Test - fun `clicking on logout sends a LogoutEvents`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + fun `clicking on logout sends a LogoutEvent`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() setLogoutView( aLogoutState( eventSink = eventsRecorder ), ) clickOn(CommonStrings.action_signout) - eventsRecorder.assertSingle(LogoutEvents.Logout(false)) + eventsRecorder.assertSingle(LogoutEvent.Logout(false)) } @Test - fun `confirming logout sends a LogoutEvents`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + fun `confirming logout sends a LogoutEvent`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() setLogoutView( aLogoutState( logoutAction = AsyncAction.ConfirmingNoParams, @@ -49,12 +49,12 @@ class LogoutViewTest : RobolectricTest() { ), ) pressTag(TestTags.dialogPositive.value) - eventsRecorder.assertSingle(LogoutEvents.Logout(false)) + eventsRecorder.assertSingle(LogoutEvent.Logout(false)) } @Test fun `clicking on back invoke back callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> setLogoutView( aLogoutState( @@ -67,8 +67,8 @@ class LogoutViewTest : RobolectricTest() { } @Test - fun `clicking on confirm after error sends a LogoutEvents`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + fun `clicking on confirm after error sends a LogoutEvent`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() setLogoutView( aLogoutState( logoutAction = AsyncAction.Failure(Exception("Failed to logout")), @@ -76,12 +76,12 @@ class LogoutViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_signout_anyway) - eventsRecorder.assertSingle(LogoutEvents.Logout(true)) + eventsRecorder.assertSingle(LogoutEvent.Logout(true)) } @Test - fun `clicking on cancel after error sends a LogoutEvents`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + fun `clicking on cancel after error sends a LogoutEvent`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() setLogoutView( aLogoutState( logoutAction = AsyncAction.Failure(Exception("Failed to logout")), @@ -89,12 +89,12 @@ class LogoutViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_cancel) - eventsRecorder.assertSingle(LogoutEvents.CloseDialogs) + eventsRecorder.assertSingle(LogoutEvent.CloseDialogs) } @Test fun `last session setting button invoke onChangeRecoveryKeyClicked`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> setLogoutView( aLogoutState( diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutViewTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutViewTest.kt index db8eed2a54..a413ba4e36 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutViewTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DefaultDirectLogoutViewTest.kt @@ -14,7 +14,7 @@ import androidx.activity.ComponentActivity import androidx.compose.ui.test.AndroidComposeUiTest import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.v2.runAndroidComposeUiTest -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.AsyncAction @@ -29,7 +29,7 @@ import org.junit.Test class DefaultDirectLogoutViewTest : RobolectricTest() { @Test fun `clicking on confirm logout sends expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.ConfirmingNoParams, @@ -37,12 +37,12 @@ class DefaultDirectLogoutViewTest : RobolectricTest() { ) ) clickOn(CommonStrings.action_signout) - eventsRecorder.assertSingle(DirectLogoutEvents.Logout(false)) + eventsRecorder.assertSingle(DirectLogoutEvent.Logout(false)) } @Test fun `clicking on cancel logout sends expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.ConfirmingNoParams, @@ -50,13 +50,13 @@ class DefaultDirectLogoutViewTest : RobolectricTest() { ) ) clickOn(CommonStrings.action_cancel) - eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs) + eventsRecorder.assertSingle(DirectLogoutEvent.CloseDialogs) } @Ignore("Pressing back key should dismiss the dialog, and so generate the expected event, but it's not the case.") @Test fun `clicking on back invoke back callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.ConfirmingNoParams, @@ -64,12 +64,12 @@ class DefaultDirectLogoutViewTest : RobolectricTest() { ) ) pressBackKey() - eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs) + eventsRecorder.assertSingle(DirectLogoutEvent.CloseDialogs) } @Test fun `clicking on confirm after error sends expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.Failure(Exception("Error")), @@ -77,12 +77,12 @@ class DefaultDirectLogoutViewTest : RobolectricTest() { ) ) clickOn(CommonStrings.action_signout_anyway) - eventsRecorder.assertSingle(DirectLogoutEvents.Logout(true)) + eventsRecorder.assertSingle(DirectLogoutEvent.Logout(true)) } @Test fun `clicking on cancel after error sends expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setDefaultDirectLogoutView( state = aDirectLogoutState( logoutAction = AsyncAction.Failure(Exception("Error")), @@ -90,7 +90,7 @@ class DefaultDirectLogoutViewTest : RobolectricTest() { ) ) clickOn(CommonStrings.action_cancel) - eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs) + eventsRecorder.assertSingle(DirectLogoutEvent.CloseDialogs) } } diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenterTest.kt index 835ed4e94e..d11c31cf1b 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/direct/DirectLogoutPresenterTest.kt @@ -13,7 +13,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test 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.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.encryption.BackupUploadState @@ -87,10 +87,10 @@ class DirectLogoutPresenterTest { presenter.present() }.test { val initialState = awaitFirstItem() - initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) + initialState.eventSink.invoke(DirectLogoutEvent.Logout(ignoreSdkError = false)) val confirmationState = awaitItem() assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams) - initialState.eventSink.invoke(DirectLogoutEvents.CloseDialogs) + initialState.eventSink.invoke(DirectLogoutEvent.CloseDialogs) val finalState = awaitItem() assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized) } @@ -103,10 +103,10 @@ class DirectLogoutPresenterTest { presenter.present() }.test { val initialState = awaitFirstItem() - initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) + initialState.eventSink.invoke(DirectLogoutEvent.Logout(ignoreSdkError = false)) val confirmationState = awaitItem() assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams) - confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) + confirmationState.eventSink.invoke(DirectLogoutEvent.Logout(ignoreSdkError = false)) val loadingState = awaitItem() assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) val successState = awaitItem() @@ -128,15 +128,15 @@ class DirectLogoutPresenterTest { presenter.present() }.test { val initialState = awaitFirstItem() - initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) + initialState.eventSink.invoke(DirectLogoutEvent.Logout(ignoreSdkError = false)) val confirmationState = awaitItem() assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams) - confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) + confirmationState.eventSink.invoke(DirectLogoutEvent.Logout(ignoreSdkError = false)) val loadingState = awaitItem() assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) val errorState = awaitItem() assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION)) - errorState.eventSink.invoke(DirectLogoutEvents.CloseDialogs) + errorState.eventSink.invoke(DirectLogoutEvent.CloseDialogs) val finalState = awaitItem() assertThat(finalState.logoutAction).isEqualTo(AsyncAction.Uninitialized) } @@ -158,15 +158,15 @@ class DirectLogoutPresenterTest { presenter.present() }.test { val initialState = awaitFirstItem() - initialState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) + initialState.eventSink.invoke(DirectLogoutEvent.Logout(ignoreSdkError = false)) val confirmationState = awaitItem() assertThat(confirmationState.logoutAction).isEqualTo(AsyncAction.ConfirmingNoParams) - confirmationState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = false)) + confirmationState.eventSink.invoke(DirectLogoutEvent.Logout(ignoreSdkError = false)) val loadingState = awaitItem() assertThat(loadingState.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) val errorState = awaitItem() assertThat(errorState.logoutAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION)) - errorState.eventSink.invoke(DirectLogoutEvents.Logout(ignoreSdkError = true)) + errorState.eventSink.invoke(DirectLogoutEvent.Logout(ignoreSdkError = true)) val loadingState2 = awaitItem() assertThat(loadingState2.logoutAction).isInstanceOf(AsyncAction.Loading::class.java) val successState = awaitItem() diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerStateProvider.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerStatePreviewParam.kt similarity index 93% rename from features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerStateProvider.kt rename to features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerStatePreviewParam.kt index 7b264a3ddb..2cf698dd84 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerStateProvider.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/timeline/voicemessages/composer/VoiceMessageComposerStatePreviewParam.kt @@ -13,7 +13,7 @@ import io.element.android.libraries.designsystem.components.media.WaveFormSample import io.element.android.libraries.textcomposer.model.VoiceMessageState import kotlin.time.Duration.Companion.seconds -open class VoiceMessageComposerStateProvider : PreviewParameterProvider { +open class VoiceMessageComposerStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aVoiceMessageComposerState(voiceMessageState = VoiceMessageState.Recording(duration = 61.seconds, levels = WaveFormSamples.allRangeWaveForm)), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index d4fce982e8..97d3465ce7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -43,7 +43,7 @@ import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPr import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.roommembermoderation.api.ModerationAction -import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent import io.element.android.features.roommembermoderation.api.RoomMemberModerationRenderer import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.androidutils.system.openUrlInExternalApp @@ -348,7 +348,7 @@ class MessagesNode( onSelectAction = { action, target -> when (action) { is ModerationAction.DisplayProfile -> callback.navigateToRoomMemberDetails(target.userId) - else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target)) + else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvent.ProcessAction(action, target)) } }, onAvatarClick = { user -> diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index dd3afb0ea6..3ba8099ecb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -54,7 +54,7 @@ import io.element.android.features.messages.impl.timeline.model.event.captionOrN import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.voicemessages.composer.DefaultVoiceMessageComposerPresenter import io.element.android.features.roomcall.api.RoomCallState -import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.androidutils.clipboard.ClipboardHelper import io.element.android.libraries.architecture.AsyncData @@ -263,7 +263,7 @@ class MessagesPresenter( } } is MessagesEvent.OnUserClicked -> { - roomMemberModerationState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(event.user)) + roomMemberModerationState.eventSink(RoomMemberModerationEvent.ShowActionsForUser(event.user)) } MessagesEvent.StopLiveLocationShare -> { localCoroutineScope.launch { @@ -400,9 +400,23 @@ class MessagesPresenter( TimelineItemAction.Pin -> handlePinAction(targetEvent) TimelineItemAction.Unpin -> handleUnpinAction(targetEvent) TimelineItemAction.ViewInTimeline -> Unit + TimelineItemAction.RetrySending -> handleRetrySending(targetEvent) } } + private suspend fun handleRetrySending(targetEvent: TimelineItem.Event) { + val sendHandle = targetEvent.sendhandle ?: return Unit.also { + Timber.w("No send handle for event ${targetEvent.eventOrTransactionId}") + } + sendHandle.retry() + .onSuccess { + Timber.d("Succeed to add the message back to the send queue") + } + .onFailure { + Timber.e(it, "Failed to add the message back to the send queue") + } + } + private suspend fun handleRemoveCaption(targetEvent: TimelineItem.Event) { timelineController.invokeOnCurrentTimeline { editCaption( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStatePreviewParam.kt similarity index 98% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStatePreviewParam.kt index 7315ace96e..d792ab5ed4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStatePreviewParam.kt @@ -38,7 +38,7 @@ import io.element.android.features.messages.impl.timeline.protection.TimelinePro import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.roomcall.api.aStandByCallState -import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData @@ -57,7 +57,7 @@ import io.element.android.libraries.textcomposer.model.aTextEditorStateRich import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentSetOf -open class MessagesStateProvider : PreviewParameterProvider { +open class MessagesStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMessagesState(), @@ -168,7 +168,7 @@ fun aRoomMemberModerationState( permissions: RoomMemberModerationPermissions = RoomMemberModerationPermissions.DEFAULT, ) = object : RoomMemberModerationState { override val permissions: RoomMemberModerationPermissions = permissions - override val eventSink: (RoomMemberModerationEvents) -> Unit = {} + override val eventSink: (RoomMemberModerationEvent) -> Unit = {} } fun aUserEventPermissions( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 5911de7372..16ca023d50 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -93,6 +93,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent +import io.element.android.features.messages.impl.timeline.sendfailure.SendFailureDialogView import io.element.android.features.messages.impl.topbars.MessagesViewTopBar import io.element.android.features.messages.impl.topbars.ThreadTopBar import io.element.android.features.messages.impl.voicemessages.composer.VoiceMessagePermissionRationaleDialog @@ -178,8 +179,8 @@ fun MessagesView( fun onContentClick(event: TimelineItem.Event) { Timber.v("onMessageClick= ${event.id}") - val eventId = event.eventId ?: return - if (eventContentValidationState[eventId].getCurrentOverallState() != ContentValidationValue.Valid) return + val eventId = event.eventId + if (eventId != null && eventContentValidationState[eventId].getCurrentOverallState() != ContentValidationValue.Valid) return val hideKeyboard = onEventContentClick(state.timelineState.isLive, event) if (hideKeyboard) { @@ -429,6 +430,31 @@ fun MessagesView( }, state = state.linkState, ) + + SendFailureDialogView( + sendFailureDialogState = state.timelineState.sendFailureDialogState, + onDismiss = { + state.timelineState.eventSink(TimelineEvent.HideSendFailureDialog) + }, + onRetry = { event -> + state.eventSink( + MessagesEvent.HandleAction( + action = TimelineItemAction.RetrySending, + event = event, + ) + ) + state.timelineState.eventSink(TimelineEvent.HideSendFailureDialog) + }, + onRemoveMessage = { event -> + state.eventSink( + MessagesEvent.HandleAction( + action = TimelineItemAction.Redact, + event = event, + ) + ) + state.timelineState.eventSink(TimelineEvent.HideSendFailureDialog) + }, + ) } @Composable @@ -590,13 +616,19 @@ private fun MessagesViewComposerBottomSheetContents( when { state.successorRoom != null -> { SuccessorRoomBanner( - modifier = Modifier.fillMaxWidth().padding(contentPadding), + modifier = Modifier + .fillMaxWidth() + .padding(contentPadding), roomSuccessor = state.successorRoom, onRoomSuccessorClick = onRoomSuccessorClick ) } state.userEventPermissions.canSendMessage -> { - Column(modifier = Modifier.fillMaxWidth().padding(contentPadding)) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(contentPadding) + ) { // Do not show the identity change if user is composing a Rich message or is seeing suggestion(s). if (state.composerState.suggestions.isEmpty() && state.composerState.textEditorState is TextEditorState.Markdown) { @@ -662,7 +694,7 @@ private fun SuccessorRoomBanner( @PreviewsDayNight @Composable -internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) state: MessagesState) = ElementPreview { +internal fun MessagesViewPreview(@PreviewParameter(MessagesStatePreviewParam::class) state: MessagesState) = ElementPreview { MessagesView( state = state, onBackClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStatePreviewParam.kt similarity index 99% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStatePreviewParam.kt index 56bac5be33..8474b4833d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStatePreviewParam.kt @@ -28,7 +28,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -open class ActionListStateProvider : PreviewParameterProvider { +open class ActionListStatePreviewParam : PreviewParameterProvider { private val suggestedEmojis = persistentListOf("👍️", "👎️", "🔥", "❤️", "👏") override val values: Sequence diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index 669a696111..24a325b641 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -514,7 +514,7 @@ private fun EmojiButton( @PreviewsDayNight @Composable internal fun ActionListViewContentPreview( - @PreviewParameter(ActionListStateProvider::class) state: ActionListState + @PreviewParameter(ActionListStatePreviewParam::class) state: ActionListState ) = ElementPreview { ActionListViewContent( state = state, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt index 25e75beb99..3c64223e59 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt @@ -36,4 +36,7 @@ enum class TimelineItemAction( EndPoll(CommonStrings.action_end_poll, CompoundDrawables.ic_compound_polls_end), Pin(CommonStrings.action_pin, CompoundDrawables.ic_compound_pin), Unpin(CommonStrings.action_unpin, CompoundDrawables.ic_compound_unpin), + + // Note: this action is not used in UI for now + RetrySending(CommonStrings.action_retry, CompoundDrawables.ic_compound_send_solid), } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparator.kt index 0a0d9b17af..2f6fa4e4e0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparator.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparator.kt @@ -32,6 +32,8 @@ class TimelineItemActionComparator : Comparator { TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, TimelineItemAction.Redact, + // Not rendered in the action list, but has to be listed here for exhaustiveness. + TimelineItemAction.RetrySending, ) override fun compare(o1: TimelineItemAction, o2: TimelineItemAction): Int { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStatePreviewParam.kt similarity index 98% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStatePreviewParam.kt index a7d4e14436..cdcc37008d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStatePreviewParam.kt @@ -30,7 +30,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import java.io.File -open class AttachmentsPreviewStateProvider : PreviewParameterProvider { +open class AttachmentsPreviewStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( anAttachmentsPreviewState(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt index 077d086720..86b8a85557 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt @@ -553,7 +553,7 @@ private fun AttachmentsPreviewBottomActions( // Only preview in dark, dark theme is forced on the Node. @Preview @Composable -internal fun AttachmentsPreviewViewPreview(@PreviewParameter(AttachmentsPreviewStateProvider::class) state: AttachmentsPreviewState) = ElementPreviewDark { +internal fun AttachmentsPreviewViewPreview(@PreviewParameter(AttachmentsPreviewStatePreviewParam::class) state: AttachmentsPreviewState) = ElementPreviewDark { AttachmentsPreviewView( state = state, localMediaRenderer = SampleMediaRenderer(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorStatePreviewParam.kt similarity index 97% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorStateProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorStatePreviewParam.kt index 7c542fd1a8..fb9901e7ec 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorStatePreviewParam.kt @@ -12,7 +12,7 @@ import androidx.core.net.toUri import io.element.android.libraries.mediaviewer.api.anImageMediaInfo import io.element.android.libraries.mediaviewer.api.local.LocalMedia -open class AttachmentImageEditorStateProvider : PreviewParameterProvider { +open class AttachmentImageEditorStatePreviewParam : PreviewParameterProvider { private val caterpillarCrop = NormalizedCropRect( left = 0.3f, top = 0.3f, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorView.kt index a5317b96c6..70a3375fa8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/imageeditor/AttachmentImageEditorView.kt @@ -676,7 +676,7 @@ private fun DrawScope.drawEdgeHandle( @Preview @Composable internal fun AttachmentImageEditorViewPreview( - @PreviewParameter(AttachmentImageEditorStateProvider::class) state: AttachmentImageEditorState, + @PreviewParameter(AttachmentImageEditorStatePreviewParam::class) state: AttachmentImageEditorState, ) = ElementPreviewDark { AttachmentImageEditorView( state = state, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePreviewParam.kt similarity index 97% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePreviewParam.kt index be53de5f66..bc1d587709 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStatePreviewParam.kt @@ -18,7 +18,7 @@ import io.element.android.libraries.matrix.ui.room.IdentityRoomMember import io.element.android.libraries.matrix.ui.room.RoomMemberIdentityStateChange import kotlinx.collections.immutable.toImmutableList -class IdentityChangeStateProvider : PreviewParameterProvider { +class IdentityChangeStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( anIdentityChangeState(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt index 21684206b5..1c7bb38588 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/IdentityChangeStateView.kt @@ -122,7 +122,7 @@ private fun ViolationAlert( @PreviewsDayNight @Composable internal fun IdentityChangeStateViewPreview( - @PreviewParameter(IdentityChangeStateProvider::class) state: IdentityChangeState, + @PreviewParameter(IdentityChangeStatePreviewParam::class) state: IdentityChangeState, ) = ElementPreview { IdentityChangeStateView( state = state, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt index 4e9bf9249a..5e5afb7f60 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/identity/MessagesViewWithIdentityChangePreview.kt @@ -20,7 +20,7 @@ import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown @PreviewsDayNight @Composable internal fun MessagesViewWithIdentityChangePreview( - @PreviewParameter(IdentityChangeStateProvider::class) identityChangeState: IdentityChangeState + @PreviewParameter(IdentityChangeStatePreviewParam::class) identityChangeState: IdentityChangeState ) = ElementPreview { MessagesView( state = aMessagesState( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureStatePreviewParam.kt similarity index 93% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureStateProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureStatePreviewParam.kt index ee4b749c8f..624882b509 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureStatePreviewParam.kt @@ -13,7 +13,7 @@ import io.element.android.features.messages.impl.crypto.sendfailure.VerifiedUser import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE -open class ResolveVerifiedUserSendFailureStateProvider : PreviewParameterProvider { +open class ResolveVerifiedUserSendFailureStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aResolveVerifiedUserSendFailureState(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureView.kt index a430d4621f..2d745df474 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureView.kt @@ -158,7 +158,7 @@ private fun VerifiedUserSendFailure.resolveAction(): String { @PreviewsDayNight @Composable internal fun ResolveVerifiedUserSendFailureViewPreview( - @PreviewParameter(ResolveVerifiedUserSendFailureStateProvider::class) state: ResolveVerifiedUserSendFailureState + @PreviewParameter(ResolveVerifiedUserSendFailureStatePreviewParam::class) state: ResolveVerifiedUserSendFailureState ) = ElementPreview { ResolveVerifiedUserSendFailureView(state) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesBindsModule.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesBindingContainer.kt similarity index 98% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesBindsModule.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesBindingContainer.kt index a345e09fa2..7e67c027d7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesBindsModule.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesBindingContainer.kt @@ -34,7 +34,7 @@ import io.element.android.libraries.di.RoomScope @ContributesTo(RoomScope::class) @BindingContainer -interface MessagesBindsModule { +interface MessagesBindingContainer { @Binds fun bindPinnedMessagesBannerPresenter(presenter: PinnedMessagesBannerPresenter): Presenter diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesProvidesModule.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesProvidesBindingContainer.kt similarity index 85% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesProvidesModule.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesProvidesBindingContainer.kt index 856a14cdb0..f66c26061b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesProvidesModule.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesProvidesBindingContainer.kt @@ -18,8 +18,8 @@ import io.element.android.libraries.matrix.api.timeline.Timeline @ContributesTo(RoomScope::class) @BindingContainer -object MessagesProvidesModule { +object MessagesProvidesBindingContainer { @Provides @LiveTimeline - fun provideLiveTimeline(joinedRoom: JoinedRoom): Timeline = joinedRoom.liveTimeline + fun providesLiveTimeline(joinedRoom: JoinedRoom): Timeline = joinedRoom.liveTimeline } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkStatePreviewParam.kt similarity index 93% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkStateProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkStatePreviewParam.kt index 2e021ec276..f72d9e2517 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkStatePreviewParam.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction import io.element.android.wysiwyg.link.Link -open class LinkStateProvider : PreviewParameterProvider { +open class LinkStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aLinkState(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkView.kt index 69e6ee8673..950765f40f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/link/LinkView.kt @@ -66,7 +66,7 @@ fun LinkView( @PreviewsDayNight @Composable -internal fun LinkViewPreview(@PreviewParameter(LinkStateProvider::class) state: LinkState) = ElementPreview { +internal fun LinkViewPreview(@PreviewParameter(LinkStatePreviewParam::class) state: LinkState) = ElementPreview { LinkView( state = state, onLinkValid = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 5f12bf4be1..2974b99696 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -86,6 +86,7 @@ import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState import io.element.android.libraries.textcomposer.model.Message import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.Suggestion +import io.element.android.libraries.textcomposer.model.SuggestionType import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.libraries.textcomposer.model.rememberMarkdownTextEditorState import io.element.android.services.analytics.api.AnalyticsService @@ -103,7 +104,9 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -459,6 +462,14 @@ class MessageComposerPresenter( val mentionTriggerFlow = merge(mentionStartTrigger, mentionCompletionTrigger) + // Refresh the room members, which are only loaded on demand, when a mention starts + launch { + suggestionSearchTrigger + .distinctUntilChangedBy { it?.type } + .filter { it?.type == SuggestionType.Mention } + .collect { room.updateMembers() } + } + val roomAliasSuggestionsFlow = roomAliasSuggestionsDataSource .getAllRoomAliasSuggestions() .stateIn(this, SharingStarted.Lazily, emptyList()) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStatePreviewParam.kt similarity index 95% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStatePreviewParam.kt index b08fea28ec..31ef5fed50 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStatePreviewParam.kt @@ -18,7 +18,7 @@ import io.element.android.wysiwyg.display.TextDisplay import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -open class MessageComposerStateProvider : PreviewParameterProvider { +open class MessageComposerStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMessageComposerState(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt index cd20f3e58b..58842b6824 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerEvent import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerState -import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerStateProvider +import io.element.android.features.messages.api.timeline.voicemessages.composer.VoiceMessageComposerStatePreviewParam import io.element.android.features.messages.api.timeline.voicemessages.composer.aVoiceMessageComposerState import io.element.android.libraries.designsystem.components.async.AsyncActionView import io.element.android.libraries.designsystem.preview.ElementPreview @@ -129,7 +129,7 @@ internal fun MessageComposerView( @PreviewsDayNight @Composable internal fun MessageComposerViewPreview( - @PreviewParameter(MessageComposerStateProvider::class) state: MessageComposerState, + @PreviewParameter(MessageComposerStatePreviewParam::class) state: MessageComposerState, ) = ElementPreview { Column { MessageComposerView( @@ -149,7 +149,7 @@ internal fun MessageComposerViewPreview( @PreviewsDayNight @Composable internal fun MessageComposerViewVoicePreview( - @PreviewParameter(VoiceMessageComposerStateProvider::class) state: VoiceMessageComposerState, + @PreviewParameter(VoiceMessageComposerStatePreviewParam::class) state: VoiceMessageComposerState, ) = ElementPreview { Column { MessageComposerView( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerStatePreviewParam.kt similarity index 96% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerStateProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerStatePreviewParam.kt index 2a2c30bf6b..37660d0000 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerStatePreviewParam.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.matrix.api.core.EventId import kotlin.random.Random -internal class PinnedMessagesBannerStateProvider : PreviewParameterProvider { +internal class PinnedMessagesBannerStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aHiddenPinnedMessagesBannerState(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerView.kt index bc80d6b014..70a98d4b4e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerView.kt @@ -294,7 +294,9 @@ private class ExitOnScrollBehavior : PinnedMessagesBannerViewScrollBehavior { @PreviewsDayNight @Composable -internal fun PinnedMessagesBannerViewPreview(@PreviewParameter(PinnedMessagesBannerStateProvider::class) state: PinnedMessagesBannerState) = ElementPreview { +internal fun PinnedMessagesBannerViewPreview(@PreviewParameter( + PinnedMessagesBannerStatePreviewParam::class +) state: PinnedMessagesBannerState) = ElementPreview { PinnedMessagesBannerView( state = state, onClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvent.kt index 73ba08b29d..9be11d244a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListEvent.kt @@ -11,8 +11,10 @@ package io.element.android.features.messages.impl.pinned.list import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.matrix.api.core.ThreadId +import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId sealed interface PinnedMessagesListEvent { data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : PinnedMessagesListEvent data class OpenThread(val threadRootId: ThreadId) : PinnedMessagesListEvent + data class ToggleReaction(val emoji: String, val eventOrTransactionId: EventOrTransactionId) : PinnedMessagesListEvent } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt index 06d97f167c..36ae68de9b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListNode.kt @@ -30,6 +30,7 @@ import io.element.android.libraries.androidutils.system.copyToClipboard import io.element.android.libraries.androidutils.system.openUrlInExternalApp import io.element.android.libraries.architecture.callback import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.UserId @@ -50,6 +51,7 @@ class PinnedMessagesListNode( actionListPresenterFactory: ActionListPresenter.Factory, private val timelineItemPresenterFactories: TimelineItemPresenterFactories, private val permalinkParser: PermalinkParser, + private val emojiPickerRenderer: EmojiPickerRenderer, ) : Node(buildContext, plugins = plugins), PinnedMessagesListNavigator { interface Callback : Plugin { fun handleEventClick(event: TimelineItem.Event, canUseOverlay: Boolean) @@ -134,6 +136,7 @@ class PinnedMessagesListNode( toastMessage = toastMessage, ) }, + emojiPickerRenderer = emojiPickerRenderer, modifier = modifier ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt index e1ee18e920..748fa084f8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt @@ -30,6 +30,8 @@ import io.element.android.features.messages.impl.link.LinkState import io.element.android.features.messages.impl.pinned.DefaultPinnedEventsTimelineProvider import io.element.android.features.messages.impl.pinned.keepDisplayablePinnedEvents import io.element.android.features.messages.impl.timeline.TimelineRoomInfo +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState +import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactoryConfig import io.element.android.features.messages.impl.timeline.model.TimelineItem @@ -47,6 +49,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsState import io.element.android.libraries.matrix.api.room.roomMembers +import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction @@ -71,6 +74,8 @@ class PinnedMessagesListPresenter( private val linkPresenter: Presenter, private val snackbarDispatcher: SnackbarDispatcher, @Assisted private val actionListPresenter: Presenter, + private val customReactionPresenter: Presenter, + private val reactionSummaryPresenter: Presenter, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, private val analyticsService: AnalyticsService, @@ -88,7 +93,7 @@ class PinnedMessagesListPresenter( private val timelineItemsFactory: TimelineItemsFactory = timelineItemsFactoryCreator.create( config = TimelineItemsFactoryConfig( computeReadReceipts = false, - computeReactions = false, + computeReactions = true, ) ) @@ -96,6 +101,9 @@ class PinnedMessagesListPresenter( override fun present(): PinnedMessagesListState { htmlConverterProvider.Update() val roomInfo by room.roomInfoFlow.collectAsState() + val userEventPermissions by room.permissionsAsState(UserEventPermissions.DEFAULT) { perms -> + perms.userEventPermissions() + } val timelineRoomInfo by remember { derivedStateOf { TimelineRoomInfo( @@ -103,7 +111,7 @@ class PinnedMessagesListPresenter( name = roomInfo.name, // We don't need to compute those values userHasPermissionToSendMessage = false, - userHasPermissionToSendReaction = false, + userHasPermissionToSendReaction = userEventPermissions.canSendReaction, // We do not care about the call state here. roomCallState = aStandByCallState(), // don't compute this value or the pin icon will be shown @@ -119,9 +127,8 @@ class PinnedMessagesListPresenter( } val timelineProtectionState = timelineProtectionPresenter.present() val linkState = linkPresenter.present() - val userEventPermissions by room.permissionsAsState(UserEventPermissions.DEFAULT) { perms -> - perms.userEventPermissions() - } + val customReactionState = customReactionPresenter.present() + val reactionSummaryState = reactionSummaryPresenter.present() val displayThreadSummaries by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Threads).collectAsState(false) @@ -138,6 +145,7 @@ class PinnedMessagesListPresenter( when (event) { is PinnedMessagesListEvent.HandleAction -> sessionCoroutineScope.handleTimelineAction(event.action, event.event) is PinnedMessagesListEvent.OpenThread -> navigator.navigateToThread(event.threadRootId) + is PinnedMessagesListEvent.ToggleReaction -> sessionCoroutineScope.toggleReaction(event.emoji, event.eventOrTransactionId) } } @@ -148,10 +156,20 @@ class PinnedMessagesListPresenter( displayThreadSummaries = displayThreadSummaries, userEventPermissions = userEventPermissions, timelineItems = pinnedMessageItems, + customReactionState = customReactionState, + reactionSummaryState = reactionSummaryState, eventSink = ::handleEvent, ) } + private fun CoroutineScope.toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId) { + launch { + timelineProvider.invokeOnTimeline { + toggleReaction(emoji, eventOrTransactionId) + } + } + } + private fun CoroutineScope.handleTimelineAction( action: TimelineItemAction, targetEvent: TimelineItem.Event, @@ -234,6 +252,8 @@ class PinnedMessagesListPresenter( linkState: LinkState, userEventPermissions: UserEventPermissions, timelineItems: AsyncData>, + customReactionState: CustomReactionState, + reactionSummaryState: ReactionSummaryState, eventSink: (PinnedMessagesListEvent) -> Unit ): PinnedMessagesListState { return when (timelineItems) { @@ -252,6 +272,8 @@ class PinnedMessagesListPresenter( userEventPermissions = userEventPermissions, timelineItems = timelineItems.data, actionListState = actionListState, + customReactionState = customReactionState, + reactionSummaryState = reactionSummaryState, eventSink = eventSink ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt index bf37029eff..6c55a0d90a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListState.kt @@ -16,6 +16,8 @@ import io.element.android.features.messages.impl.UserEventPermissions import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.link.LinkState import io.element.android.features.messages.impl.timeline.TimelineRoomInfo +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState +import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.libraries.ui.strings.CommonPlurals @@ -34,6 +36,8 @@ sealed interface PinnedMessagesListState { val timelineItems: ImmutableList, val actionListState: ActionListState, val linkState: LinkState, + val customReactionState: CustomReactionState, + val reactionSummaryState: ReactionSummaryState, val displayThreadSummaries: Boolean, val eventSink: (PinnedMessagesListEvent) -> Unit, ) : PinnedMessagesListState { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStatePreviewParam.kt similarity index 88% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStatePreviewParam.kt index bfa9820a44..3d01906df8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListStatePreviewParam.kt @@ -10,6 +10,8 @@ package io.element.android.features.messages.impl.pinned.list import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.messages.impl.UserEventPermissions +import io.element.android.features.messages.impl.aCustomReactionState +import io.element.android.features.messages.impl.aReactionSummaryState import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.link.LinkState @@ -19,6 +21,8 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemDaySepara import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState +import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemAudioContent @@ -30,7 +34,7 @@ import io.element.android.features.messages.impl.timeline.protection.aTimelinePr import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -open class PinnedMessagesListStateProvider : PreviewParameterProvider { +open class PinnedMessagesListStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aFailedPinnedMessagesListState(), @@ -92,6 +96,8 @@ fun aLoadedPinnedMessagesListState( linkState: LinkState = aLinkState(), timelineItems: List = emptyList(), actionListState: ActionListState = anActionListState(), + customReactionState: CustomReactionState = aCustomReactionState(), + reactionSummaryState: ReactionSummaryState = aReactionSummaryState(), aUserEventPermissions: UserEventPermissions = UserEventPermissions.DEFAULT, displayThreadSummaries: Boolean = false, eventSink: (PinnedMessagesListEvent) -> Unit = {} @@ -101,6 +107,8 @@ fun aLoadedPinnedMessagesListState( linkState = linkState, timelineItems = timelineItems.toImmutableList(), actionListState = actionListState, + customReactionState = customReactionState, + reactionSummaryState = reactionSummaryState, userEventPermissions = aUserEventPermissions, displayThreadSummaries = displayThreadSummaries, eventSink = eventSink, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index 4f69bacd28..5daec3e275 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -33,8 +33,12 @@ import io.element.android.features.messages.impl.link.LinkEvent import io.element.android.features.messages.impl.link.LinkView import io.element.android.features.messages.impl.timeline.TimelineEvent import io.element.android.features.messages.impl.timeline.components.TimelineItemRow +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvent import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData +import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvent +import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState @@ -50,6 +54,8 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.lazyColumnContentPadding import io.element.android.libraries.designsystem.utils.scaffoldScrollableContentInsets +import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer +import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.ui.strings.CommonStrings @@ -66,6 +72,7 @@ fun PinnedMessagesListView( onUserDataClick: (MatrixUser) -> Unit, onLinkClick: (Link) -> Unit, onLinkLongClick: (Link) -> Unit, + emojiPickerRenderer: EmojiPickerRenderer, modifier: Modifier = Modifier, ) { Scaffold( @@ -89,6 +96,7 @@ fun PinnedMessagesListView( onLinkClick = onLinkClick, onLinkLongClick = onLinkLongClick, onErrorDismiss = onBackClick, + emojiPickerRenderer = emojiPickerRenderer, modifier = Modifier .padding(padding) .consumeWindowInsets(padding), @@ -121,6 +129,7 @@ private fun PinnedMessagesListContent( onLinkClick: (Link) -> Unit, onLinkLongClick: (Link) -> Unit, onErrorDismiss: () -> Unit, + emojiPickerRenderer: EmojiPickerRenderer, modifier: Modifier = Modifier, ) { Box(modifier.fillMaxSize()) { @@ -141,6 +150,7 @@ private fun PinnedMessagesListContent( onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, onLinkLongClick = onLinkLongClick, + emojiPickerRenderer = emojiPickerRenderer, ) PinnedMessagesListState.Loading -> { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { @@ -180,6 +190,7 @@ private fun PinnedMessagesListLoaded( onUserDataClick: (MatrixUser) -> Unit, onLinkClick: (Link) -> Unit, onLinkLongClick: (Link) -> Unit, + emojiPickerRenderer: EmojiPickerRenderer, modifier: Modifier = Modifier, ) { fun onActionSelected(timelineItemAction: TimelineItemAction, event: TimelineItem.Event) { @@ -203,11 +214,25 @@ private fun PinnedMessagesListLoaded( ) } + fun onReactionClick(emoji: String, event: TimelineItem.Event) { + state.eventSink(PinnedMessagesListEvent.ToggleReaction(emoji, event.eventOrTransactionId)) + } + + fun onReactionLongClick(emoji: String, event: TimelineItem.Event) { + event.eventId?.let { eventId -> + state.reactionSummaryState.eventSink(ReactionSummaryEvent.ShowReactionSummary(eventId, event.reactionsState.reactions, emoji)) + } + } + + fun onMoreReactionsClick(event: TimelineItem.Event) { + state.customReactionState.eventSink(CustomReactionEvent.ShowCustomReactionSheet(event)) + } + ActionListView( state = state.actionListState, onSelectAction = ::onActionSelected, - onCustomReactionClick = {}, - onEmojiReactionClick = { _, _ -> }, + onCustomReactionClick = ::onMoreReactionsClick, + onEmojiReactionClick = ::onReactionClick, onVerifiedUserSendFailureClick = {} ) @@ -239,9 +264,9 @@ private fun PinnedMessagesListLoaded( onLongClick = ::onMessageLongClick, displayThreadSummaries = displayThreadSummaries, inReplyToClick = {}, - onReactionClick = { _, _ -> }, - onReactionLongClick = { _, _ -> }, - onMoreReactionsClick = {}, + onReactionClick = ::onReactionClick, + onReactionLongClick = ::onReactionLongClick, + onMoreReactionsClick = ::onMoreReactionsClick, onReadReceiptClick = {}, onSwipeToReply = {}, onJoinCallClick = {}, @@ -273,6 +298,14 @@ private fun PinnedMessagesListLoaded( state.linkState, onLinkValid = onLinkClick, ) + CustomReactionBottomSheet( + state = state.customReactionState, + onSelectEmoji = { eventOrTransactionId, emoji -> + state.eventSink(PinnedMessagesListEvent.ToggleReaction(emoji.unicode, eventOrTransactionId)) + }, + emojiPickerRenderer = emojiPickerRenderer, + ) + ReactionSummaryView(state = state.reactionSummaryState) } @Composable @@ -312,7 +345,7 @@ private fun TimelineItemEventContentViewWrapper( @PreviewsDayNight @Composable -internal fun PinnedMessagesListViewPreview(@PreviewParameter(PinnedMessagesListStateProvider::class) state: PinnedMessagesListState) = +internal fun PinnedMessagesListViewPreview(@PreviewParameter(PinnedMessagesListStatePreviewParam::class) state: PinnedMessagesListState) = ElementPreview { PinnedMessagesListView( state = state, @@ -322,5 +355,6 @@ internal fun PinnedMessagesListViewPreview(@PreviewParameter(PinnedMessagesListS onUserDataClick = {}, onLinkClick = {}, onLinkLongClick = {}, + emojiPickerRenderer = NoOpEmojiPickerRenderer, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStatePreviewParam.kt similarity index 94% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStatePreviewParam.kt index 0e2b4b0c50..1f24b3f480 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStatePreviewParam.kt @@ -11,7 +11,7 @@ package io.element.android.features.messages.impl.report import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction -open class ReportMessageStateProvider : PreviewParameterProvider { +open class ReportMessageStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aReportMessageState(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageView.kt index dde3f13558..a3cf4f09da 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageView.kt @@ -137,7 +137,7 @@ fun ReportMessageView( @PreviewsDayNight @Composable -internal fun ReportMessageViewPreview(@PreviewParameter(ReportMessageStateProvider::class) state: ReportMessageState) = ElementPreview { +internal fun ReportMessageViewPreview(@PreviewParameter(ReportMessageStatePreviewParam::class) state: ReportMessageState) = ElementPreview { ReportMessageView( onBackClick = {}, state = state, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt index b7fd9f572f..cf3c701015 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/ThreadedMessagesNode.kt @@ -46,7 +46,7 @@ import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPr import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.roommembermoderation.api.ModerationAction -import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent import io.element.android.features.roommembermoderation.api.RoomMemberModerationRenderer import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.androidutils.system.openUrlInExternalApp @@ -343,7 +343,7 @@ class ThreadedMessagesNode( onSelectAction = { action, target -> when (action) { is ModerationAction.DisplayProfile -> callback.navigateToRoomMemberDetails(target.userId) - else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target)) + else -> state.roomMemberModerationState.eventSink(RoomMemberModerationEvent.ProcessAction(action, target)) } }, onAvatarClick = { user -> diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListEvent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListEvent.kt new file mode 100644 index 0000000000..191d5d111b --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListEvent.kt @@ -0,0 +1,12 @@ +/* + * 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.messages.impl.threads.list + +sealed interface ThreadsListEvent { + data object Paginate : ThreadsListEvent +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListPresenter.kt index ba10df46f2..d6d928326c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListPresenter.kt @@ -20,13 +20,10 @@ import io.element.android.features.messages.impl.utils.messagesummary.MessageSum import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.dateformatter.api.DateFormatter import io.element.android.libraries.dateformatter.api.DateFormatterMode -import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize -import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.threads.ThreadListPaginationStatus import io.element.android.libraries.matrix.ui.model.getAvatarData -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.onStart @@ -104,9 +101,9 @@ class ThreadsListPresenter( derivedStateOf { roomInfo.heroes.map { it.getAvatarData(AvatarSize.CurrentUserTopBar) }.toImmutableList() } } - fun handleEvent(event: ThreadsListEvents) { + fun handleEvent(event: ThreadsListEvent) { when (event) { - ThreadsListEvents.Paginate -> if ((paginationStatus as? ThreadListPaginationStatus.Idle)?.hasMoreToLoad == true) { + ThreadsListEvent.Paginate -> if ((paginationStatus as? ThreadListPaginationStatus.Idle)?.hasMoreToLoad == true) { coroutineScope.launch { Timber.d("Paginating thread list: $paginationStatus") threadsListService.paginate() @@ -129,17 +126,3 @@ class ThreadsListPresenter( ) } } - -data class ThreadsListState( - val roomId: RoomId, - val roomName: String, - val roomAvatarUrl: String?, - val isRoomTombstoned: Boolean, - val heroes: ImmutableList, - val threads: ImmutableList, - val eventSink: (ThreadsListEvents) -> Unit, -) - -sealed interface ThreadsListEvents { - data object Paginate : ThreadsListEvents -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListState.kt new file mode 100644 index 0000000000..8aafd49d21 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListState.kt @@ -0,0 +1,22 @@ +/* + * 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.messages.impl.threads.list + +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.collections.immutable.ImmutableList + +data class ThreadsListState( + val roomId: RoomId, + val roomName: String, + val roomAvatarUrl: String?, + val isRoomTombstoned: Boolean, + val heroes: ImmutableList, + val threads: ImmutableList, + val eventSink: (ThreadsListEvent) -> Unit, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt index 6480dacfcf..c38da19a67 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/threads/list/ThreadsListView.kt @@ -156,7 +156,7 @@ fun ThreadsListView( } ScrollHelper(lazyListState) { - state.eventSink(ThreadsListEvents.Paginate) + state.eventSink(ThreadsListEvent.Paginate) } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvent.kt index cfc3672ca3..97608e15fa 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvent.kt @@ -34,6 +34,9 @@ sealed interface TimelineEvent { */ sealed interface TimelineItemEvent : TimelineEvent + data class ShowSendFailureDialog(val event: TimelineItem.Event) : TimelineItemEvent + data object HideSendFailureDialog : TimelineEvent + data class ComputeVerifiedUserSendFailure(val event: TimelineItem.Event) : TimelineItemEvent data class ShowShieldDialog(val messageShieldData: MessageShieldData) : TimelineItemEvent data class LoadMore(val direction: Timeline.PaginationDirection) : TimelineItemEvent diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 7cd0d018d2..43913c43bb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -37,6 +37,7 @@ import io.element.android.features.messages.impl.timeline.model.virtual.Timeline import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemTypingNotificationModel import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState +import io.element.android.features.messages.impl.timeline.sendfailure.SendFailureDialogState import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.messages.impl.userEventPermissions import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager @@ -56,6 +57,7 @@ import io.element.android.libraries.matrix.api.room.powerlevels.permissionsAsSta import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction.DisplayFirstTimelineItems @@ -142,6 +144,7 @@ class TimelinePresenter( val newEventState = remember { mutableStateOf(NewEventState.None) } val messageShieldDialogData: MutableState = remember { mutableStateOf(null) } + var sendFailureDialogState by remember { mutableStateOf(SendFailureDialogState.Hidden) } // Forces [JumpToUnreadState.Hidden] until the next RoomInfo push. Set after a // [TimelineEvent.MarkAllAsRead] await completes so the FAB hides without waiting for @@ -166,9 +169,14 @@ class TimelinePresenter( fun handleEvent(event: TimelineEvent) { when (event) { is TimelineEvent.LoadMore -> { - if (event.direction == Timeline.PaginationDirection.FORWARDS && timelineMode is Timeline.Mode.Thread) { - // Do not paginate forwards in thread mode, as it's not supported - return + if (event.direction == Timeline.PaginationDirection.FORWARDS) { + if (timelineMode is Timeline.Mode.Thread) { + // Do not paginate forwards in thread mode, as it's not supported + return + } + if (focusRequestState.value.isPending()) { + return + } } localScope.launch { timelineController.paginate(direction = event.direction) @@ -244,6 +252,47 @@ class TimelinePresenter( suppressJumpToUnread.value = true } is TimelineEvent.ShowShieldDialog -> messageShieldDialogData.value = event.messageShieldData + is TimelineEvent.ShowSendFailureDialog -> { + val sendStateFailure = event.event.localSendState as? LocalEventSendState.Failed ?: return + when (sendStateFailure) { + is LocalEventSendState.Failed.VerifiedUser -> { + // defer to the resolveVerifiedUserSendFailureState to handle this case, + // as it will show a dialog with the right information + resolveVerifiedUserSendFailureState.eventSink(ResolveVerifiedUserSendFailureEvent.ComputeForMessage(event.event)) + } + is LocalEventSendState.Failed.InvalidMimeType -> { + sendFailureDialogState = SendFailureDialogState.Show( + event = event.event, + sendFailureType = SendFailureDialogState.SendFailureType.InvalidMimeType(sendStateFailure.mimeType), + ) + } + LocalEventSendState.Failed.MissingMediaContent -> { + sendFailureDialogState = SendFailureDialogState.Show( + event = event.event, + sendFailureType = SendFailureDialogState.SendFailureType.MissingMediaContent, + ) + } + LocalEventSendState.Failed.SendingFromUnverifiedDevice -> { + sendFailureDialogState = SendFailureDialogState.Show( + event = event.event, + sendFailureType = SendFailureDialogState.SendFailureType.SendingFromUnverifiedDevice, + ) + } + is LocalEventSendState.Failed.Unknown -> { + sendFailureDialogState = SendFailureDialogState.Show( + event = event.event, + sendFailureType = if (sendStateFailure.error.isNotBlank()) { + SendFailureDialogState.SendFailureType.Error(sendStateFailure.error) + } else { + SendFailureDialogState.SendFailureType.Unknown + } + ) + } + } + } + is TimelineEvent.HideSendFailureDialog -> { + sendFailureDialogState = SendFailureDialogState.Hidden + } is TimelineEvent.ComputeVerifiedUserSendFailure -> { resolveVerifiedUserSendFailureState.eventSink(ResolveVerifiedUserSendFailureEvent.ComputeForMessage(event.event)) } @@ -408,6 +457,7 @@ class TimelinePresenter( focusRequestState = focusRequestState.value, messageShieldDialogData = messageShieldDialogData.value, resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState, + sendFailureDialogState = sendFailureDialogState, displayThreadSummaries = displayThreadSummaries, displayJumpToUnread = displayJumpToUnread, jumpToUnread = jumpToUnread.value, @@ -537,6 +587,16 @@ private fun FocusRequestState.onFocusEventRender(): FocusRequestState { } } +private fun FocusRequestState.isPending(): Boolean { + return when (this) { + is FocusRequestState.Requested, + is FocusRequestState.Loading -> true + is FocusRequestState.Success -> !rendered + FocusRequestState.None, + is FocusRequestState.Failure -> false + } +} + // Workaround for not having the server names available, get possible server names from the user ids of the room members private fun calculateServerNamesForRoom(room: JoinedRoom): List { // If we have no room members, return right ahead diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt index 7836c17563..1ebc96f15f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt @@ -13,6 +13,7 @@ import io.element.android.features.messages.impl.crypto.sendfailure.resolve.Reso import io.element.android.features.messages.impl.timeline.components.MessageShieldData import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.sendfailure.SendFailureDialogState import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.roomcall.api.RoomCallState import io.element.android.libraries.matrix.api.core.EventId @@ -32,6 +33,7 @@ data class TimelineState( // If not null, info will be rendered in a dialog val messageShieldDialogData: MessageShieldData?, val resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState, + val sendFailureDialogState: SendFailureDialogState, val displayThreadSummaries: Boolean, val displayJumpToUnread: Boolean, val jumpToUnread: JumpToUnreadState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index 61b5c690a4..89545b5d0b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -25,6 +25,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel +import io.element.android.features.messages.impl.timeline.sendfailure.SendFailureDialogState import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.messages.impl.typing.aTypingNotificationState import io.element.android.features.roomcall.api.aStandByCallState @@ -57,6 +58,7 @@ fun aTimelineState( isLive: Boolean = true, messageShield: MessageShield? = null, resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState = aResolveVerifiedUserSendFailureState(), + sendFailureDialogState: SendFailureDialogState = SendFailureDialogState.Hidden, displayThreadSummaries: Boolean = false, displayJumpToUnread: Boolean = false, jumpToUnread: JumpToUnreadState = JumpToUnreadState.Hidden, @@ -78,6 +80,7 @@ fun aTimelineState( focusRequestState = focusRequestState, messageShieldDialogData = messageShield?.let { MessageShieldData(it) }, resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState, + sendFailureDialogState = sendFailureDialogState, displayThreadSummaries = displayThreadSummaries, displayJumpToUnread = displayJumpToUnread, jumpToUnread = jumpToUnread, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 78c067eed1..5e7fd34723 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -90,7 +90,7 @@ import io.element.android.features.messages.impl.timeline.focus.FocusRequestStat import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentPreviewParam import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState import io.element.android.libraries.androidutils.system.copyToClipboard @@ -582,7 +582,7 @@ private fun JumpToPositionButton( @PreviewsDayNight @Composable internal fun TimelineViewPreview( - @PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent + @PreviewParameter(TimelineItemEventContentPreviewParam::class) content: TimelineItemEventContent ) = ElementPreview { val timelineItems = aTimelineItemList(content) val timelineEvents = timelineItems.filterIsInstance() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt index 28f5011652..d4b9885ea5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/CallMenuItem.kt @@ -25,7 +25,7 @@ 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.roomcall.api.RoomCallState -import io.element.android.features.roomcall.api.RoomCallStateProvider +import io.element.android.features.roomcall.api.RoomCallStatePreviewParam import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon @@ -133,7 +133,7 @@ private fun OnGoingCallMenuItem( @PreviewsDayNight @Composable internal fun CallMenuItemPreview( - @PreviewParameter(RoomCallStateProvider::class) roomCallState: RoomCallState + @PreviewParameter(RoomCallStatePreviewParam::class) roomCallState: RoomCallState ) = ElementPreview { CallMenuItem( roomCallState = roomCallState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt index 1ebdc34c6c..0dcf489fe3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState -import io.element.android.features.messages.impl.timeline.model.bubble.BubbleStateProvider +import io.element.android.features.messages.impl.timeline.model.bubble.BubbleStatePreviewParam import io.element.android.libraries.core.extensions.to01 import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction @@ -192,7 +192,7 @@ object MessageEventBubbleDefaults { @PreviewsDayNight @Composable -internal fun MessageEventBubblePreview(@PreviewParameter(BubbleStateProvider::class) state: BubbleState) = ElementPreview { +internal fun MessageEventBubblePreview(@PreviewParameter(BubbleStatePreviewParam::class) state: BubbleState) = ElementPreview { // Due to position offset, surround with a Box Box( modifier = Modifier diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt index dc8e171298..931e0de801 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessagesReactionButton.kt @@ -41,7 +41,7 @@ import io.element.android.features.messages.impl.R import io.element.android.features.messages.impl.timeline.a11y.a11yReactionAction import io.element.android.features.messages.impl.timeline.a11y.a11yReactionDetails import io.element.android.features.messages.impl.timeline.model.AggregatedReaction -import io.element.android.features.messages.impl.timeline.model.AggregatedReactionProvider +import io.element.android.features.messages.impl.timeline.model.AggregatedReactionPreviewParam import io.element.android.features.messages.impl.timeline.model.aTimelineItemReactions import io.element.android.libraries.designsystem.icons.CompoundDrawables import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction @@ -199,7 +199,7 @@ private fun ReactionContent( @PreviewsDayNight @Composable -internal fun MessagesReactionButtonPreview(@PreviewParameter(AggregatedReactionProvider::class) reaction: AggregatedReaction) = ElementPreview { +internal fun MessagesReactionButtonPreview(@PreviewParameter(AggregatedReactionPreviewParam::class) reaction: AggregatedReaction) = ElementPreview { MessagesReactionButton( content = MessagesReactionsButtonContent.Reaction(reaction), onClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt index b6af359269..d7492ca40b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt @@ -56,10 +56,11 @@ fun TimelineEventTimestampView( val isVerifiedUserSendFailure = event.localSendState is LocalEventSendState.Failed.VerifiedUser val onClickLabel = when { shield != null -> stringResource(CommonStrings.a11y_view_details) - hasError && isVerifiedUserSendFailure -> stringResource(CommonStrings.action_open_context_menu) + isVerifiedUserSendFailure -> stringResource(CommonStrings.action_open_context_menu) + hasError -> stringResource(CommonStrings.a11y_view_details) else -> null } - val clickableModifier = remember(shield, hasError) { + val clickableModifier = remember(event) { when { shield != null -> { Modifier.clickable( @@ -70,10 +71,9 @@ fun TimelineEventTimestampView( } hasError -> Modifier .clickable( - enabled = isVerifiedUserSendFailure, onClickLabel = onClickLabel, ) { - eventSink(TimelineEvent.ComputeVerifiedUserSendFailure(event)) + eventSink(TimelineEvent.ShowSendFailureDialog(event)) } else -> Modifier } @@ -134,7 +134,9 @@ fun TimelineEventTimestampView( @PreviewsDayNight @Composable -internal fun TimelineEventTimestampViewPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) = ElementPreview { +internal fun TimelineEventTimestampViewPreview(@PreviewParameter( + TimelineItemEventForTimestampViewPreviewParam::class +) event: TimelineItem.Event) = ElementPreview { TimelineEventTimestampView( event = event, eventSink = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventContentForGalleryViewProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventContentForGalleryViewPreviewParam.kt similarity index 98% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventContentForGalleryViewProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventContentForGalleryViewPreviewParam.kt index ed790da8a2..0f7da4209f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventContentForGalleryViewProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventContentForGalleryViewPreviewParam.kt @@ -16,7 +16,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.model.event.anAttachmentItem import kotlin.time.Duration -class TimelineItemEventContentForGalleryViewProvider : +class TimelineItemEventContentForGalleryViewPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewPreviewParam.kt similarity index 95% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewPreviewParam.kt index 3689c84875..118edae373 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewPreviewParam.kt @@ -16,7 +16,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield -class TimelineItemEventForTimestampViewProvider : PreviewParameterProvider { +class TimelineItemEventForTimestampViewPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aTimelineItemEvent(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowDisambiguatedPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowDisambiguatedPreview.kt index 4b6223fd97..94d29d2191 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowDisambiguatedPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowDisambiguatedPreview.kt @@ -13,12 +13,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails -import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsDisambiguatedProvider +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsDisambiguatedPreviewParam @PreviewsDayNight @Composable internal fun TimelineItemEventRowDisambiguatedPreview( - @PreviewParameter(InReplyToDetailsDisambiguatedProvider::class) inReplyToDetails: InReplyToDetails, + @PreviewParameter(InReplyToDetailsDisambiguatedPreviewParam::class) inReplyToDetails: InReplyToDetails, ) = ElementPreview { TimelineItemEventRowWithReplyContentToPreview( inReplyToDetails = inReplyToDetails, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowGalleryPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowGalleryPreview.kt index 22c3f354f2..b942695470 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowGalleryPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowGalleryPreview.kt @@ -19,7 +19,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight @PreviewsDayNight @Composable internal fun TimelineItemEventRowWithGalleryPreview( - @PreviewParameter(TimelineItemEventContentForGalleryViewProvider::class) content: TimelineItemEventContent, + @PreviewParameter(TimelineItemEventContentForGalleryViewPreviewParam::class) content: TimelineItemEventContent, ) = ElementPreview { Column { sequenceOf(false, true).forEach { isMine -> diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowTimestampPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowTimestampPreview.kt index c588b1d071..ae0077786b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowTimestampPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowTimestampPreview.kt @@ -20,7 +20,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight @PreviewsDayNight @Composable internal fun TimelineItemEventRowTimestampPreview( - @PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event + @PreviewParameter(TimelineItemEventForTimestampViewPreviewParam::class) event: TimelineItem.Event ) = ElementPreview { Column { when (event.content) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithRRPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithRRPreview.kt index 6ea1161605..379a5b3547 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithRRPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithRRPreview.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState -import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewStateForTimelineItemEventRowProvider +import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewStateForTimelineItemEventRowPreviewParam import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.libraries.designsystem.preview.ElementPreview @@ -25,7 +25,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight @PreviewsDayNight @Composable internal fun TimelineItemEventRowWithRRPreview( - @PreviewParameter(ReadReceiptViewStateForTimelineItemEventRowProvider::class) state: ReadReceiptViewState, + @PreviewParameter(ReadReceiptViewStateForTimelineItemEventRowPreviewParam::class) state: ReadReceiptViewState, ) = ElementPreview { Column { // A message from someone else diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyInformativePreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyInformativePreview.kt index cdc60a9e7e..374561e553 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyInformativePreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyInformativePreview.kt @@ -13,12 +13,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails -import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsInformativeProvider +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsInformativePreviewParam @PreviewsDayNight @Composable internal fun TimelineItemEventRowWithReplyInformativePreview( - @PreviewParameter(InReplyToDetailsInformativeProvider::class) inReplyToDetails: InReplyToDetails, + @PreviewParameter(InReplyToDetailsInformativePreviewParam::class) inReplyToDetails: InReplyToDetails, ) = ElementPreview { TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyOtherPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyOtherPreview.kt index 3071fe114a..f7d2de2ced 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyOtherPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyOtherPreview.kt @@ -13,12 +13,12 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails -import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsOtherProvider +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsOtherPreviewParam @PreviewsDayNight @Composable internal fun TimelineItemEventRowWithReplyOtherPreview( - @PreviewParameter(InReplyToDetailsOtherProvider::class) inReplyToDetails: InReplyToDetails, + @PreviewParameter(InReplyToDetailsOtherPreviewParam::class) inReplyToDetails: InReplyToDetails, ) = ElementPreview { TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt index 7ddcadfe3c..aba6971f1d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt @@ -21,12 +21,12 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails -import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsProvider +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsPreviewParam @PreviewsDayNight @Composable internal fun TimelineItemEventRowWithReplyPreview( - @PreviewParameter(InReplyToDetailsProvider::class) inReplyToDetails: InReplyToDetails, + @PreviewParameter(InReplyToDetailsPreviewParam::class) inReplyToDetails: InReplyToDetails, ) = ElementPreview { TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentsContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentsContentPreviewParam.kt similarity index 96% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentsContentProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentsContentPreviewParam.kt index c63d68e062..a42eec8504 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentsContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentsContentPreviewParam.kt @@ -13,7 +13,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.model.event.anAttachmentItem import io.element.android.libraries.matrix.api.media.MediaSource -class TimelineItemAttachmentsContentProvider : PreviewParameterProvider { +class TimelineItemAttachmentsContentPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aTimelineItemAttachmentsContent( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentsListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentsListView.kt index f3745b2c5a..232c107698 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentsListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentsListView.kt @@ -265,7 +265,7 @@ private fun AttachmentListItem( @PreviewsDayNight @Composable internal fun TimelineItemAttachmentsListViewPreview( - @PreviewParameter(TimelineItemAttachmentsContentProvider::class) content: TimelineItemAttachmentsContent + @PreviewParameter(TimelineItemAttachmentsContentPreviewParam::class) content: TimelineItemAttachmentsContent ) = ElementPreview { TimelineItemAttachmentsListView( eventId = EventId("\$eventId"), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt index 221ba2ca6f..d47ca552b8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContentProvider +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContentPreviewParam import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue @@ -38,7 +38,7 @@ fun TimelineItemAudioView( @PreviewsDayNight @Composable -internal fun TimelineItemAudioViewPreview(@PreviewParameter(TimelineItemAudioContentProvider::class) content: TimelineItemAudioContent) = +internal fun TimelineItemAudioViewPreview(@PreviewParameter(TimelineItemAudioContentPreviewParam::class) content: TimelineItemAudioContent) = ElementTimelineItemPreview { TimelineItemAudioView( content, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEncryptedView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEncryptedView.kt index 48b3acfbb8..6b9022d3cf 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEncryptedView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEncryptedView.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContentProvider +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContentPreviewParam import io.element.android.libraries.designsystem.icons.CompoundDrawables import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -75,7 +75,7 @@ fun TimelineItemEncryptedView( @PreviewsDayNight @Composable internal fun TimelineItemEncryptedViewPreview( - @PreviewParameter(TimelineItemEncryptedContentProvider::class) content: TimelineItemEncryptedContent + @PreviewParameter(TimelineItemEncryptedContentPreviewParam::class) content: TimelineItemEncryptedContent ) = ElementPreview { TimelineItemEncryptedView( content = content, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt index cd0632282e..77df9ba923 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContentProvider +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContentPreviewParam import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue import io.element.android.libraries.ui.strings.CommonStrings @@ -43,7 +43,7 @@ fun TimelineItemFileView( @PreviewsDayNight @Composable -internal fun TimelineItemFileViewPreview(@PreviewParameter(TimelineItemFileContentProvider::class) content: TimelineItemFileContent) { +internal fun TimelineItemFileViewPreview(@PreviewParameter(TimelineItemFileContentPreviewParam::class) content: TimelineItemFileContent) { ElementTimelineItemPreview { TimelineItemFileView( content, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryContentPreviewParam.kt similarity index 97% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryContentProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryContentPreviewParam.kt index 9d88d8c0fb..9fb34afaea 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryContentPreviewParam.kt @@ -15,7 +15,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -class TimelineItemGalleryContentProvider : PreviewParameterProvider { +class TimelineItemGalleryContentPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aTimelineItemGalleryContent( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryView.kt index 3212d789f9..e433f6ef8e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemGalleryView.kt @@ -390,7 +390,7 @@ private fun RemainingCountOverlay(count: Int) { @PreviewsDayNight @Composable internal fun TimelineItemGalleryViewPreview( - @PreviewParameter(TimelineItemGalleryContentProvider::class) content: TimelineItemGalleryContent, + @PreviewParameter(TimelineItemGalleryContentPreviewParam::class) content: TimelineItemGalleryContent, ) = ElementPreview { TimelineItemGalleryView( eventId = null, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt index 3e79fd6fd9..ce12690534 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt @@ -33,7 +33,7 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.components.ATimelineItemEventRow import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentPreviewParam import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.protection.ProtectedView import io.element.android.features.messages.impl.timeline.protection.coerceRatioWhenHidingContent @@ -125,7 +125,7 @@ fun TimelineItemImageView( @PreviewsDayNight @Composable -internal fun TimelineItemImageViewPreview(@PreviewParameter(TimelineItemImageContentProvider::class) content: TimelineItemImageContent) = ElementPreview { +internal fun TimelineItemImageViewPreview(@PreviewParameter(TimelineItemImageContentPreviewParam::class) content: TimelineItemImageContent) = ElementPreview { TimelineItemImageView( content = content, hideMediaContent = false, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt index a7f7b33989..3ff8a62455 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt @@ -31,7 +31,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.location.api.LocalMapTilerConfig import io.element.android.features.location.api.StaticMapView import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContentProvider +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContentPreviewParam import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator @@ -162,7 +162,7 @@ private fun LiveLocationOverlay( @PreviewsDayNight @Composable -internal fun TimelineItemLocationViewPreview(@PreviewParameter(TimelineItemLocationContentProvider::class) content: TimelineItemLocationContent) = +internal fun TimelineItemLocationViewPreview(@PreviewParameter(TimelineItemLocationContentPreviewParam::class) content: TimelineItemLocationContent) = ElementPreview { TimelineItemLocationView( content = content, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt index e956a99b45..b6e4d12517 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.messages.impl.timeline.TimelineEvent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContentProvider +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContentPreviewParam import io.element.android.features.poll.api.pollcontent.PollContentView import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -55,7 +55,7 @@ fun TimelineItemPollView( @PreviewsDayNight @Composable -internal fun TimelineItemPollViewPreview(@PreviewParameter(TimelineItemPollContentProvider::class) content: TimelineItemPollContent) = +internal fun TimelineItemPollViewPreview(@PreviewParameter(TimelineItemPollContentPreviewParam::class) content: TimelineItemPollContent) = ElementPreview { TimelineItemPollView( content = content, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt index 7e240f8b51..c7878c00f5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt @@ -28,7 +28,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.PreviewParameter import coil3.compose.AsyncImage import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContentProvider +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContentPreviewParam import io.element.android.features.messages.impl.timeline.protection.ProtectedView import io.element.android.features.messages.impl.timeline.protection.coerceRatioWhenHidingContent import io.element.android.features.messages.impl.timeline.util.handleAsyncImageStateChange @@ -123,7 +123,9 @@ fun TimelineItemStickerView( @PreviewsDayNight @Composable -internal fun TimelineItemStickerViewPreview(@PreviewParameter(TimelineItemStickerContentProvider::class) content: TimelineItemStickerContent) = ElementPreview { +internal fun TimelineItemStickerViewPreview(@PreviewParameter( + TimelineItemStickerContentPreviewParam::class +) content: TimelineItemStickerContent) = ElementPreview { TimelineItemStickerView( content = content, hideMediaContent = false, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt index 1a4be5d2e5..a7d54e819b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt @@ -24,7 +24,7 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContentProvider +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContentPreviewParam import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.utils.containsOnlyEmojis import io.element.android.libraries.androidutils.text.LinkifyHelper @@ -82,7 +82,7 @@ internal fun getTextWithResolvedMentions(content: TimelineItemTextBasedContent): @PreviewsDayNight @Composable internal fun TimelineItemTextViewPreview( - @PreviewParameter(TimelineItemTextBasedContentProvider::class) content: TimelineItemTextBasedContent + @PreviewParameter(TimelineItemTextBasedContentPreviewParam::class) content: TimelineItemTextBasedContent ) = ElementPreview { TimelineItemTextView( content = content, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt index b47220fa5e..5b3803713e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContentProvider +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContentPreviewParam import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent import io.element.android.features.messages.impl.timeline.protection.ProtectedView import io.element.android.features.messages.impl.timeline.protection.coerceRatioWhenHidingContent @@ -150,7 +150,7 @@ fun TimelineItemVideoView( @PreviewsDayNight @Composable -internal fun TimelineItemVideoViewPreview(@PreviewParameter(TimelineItemVideoContentProvider::class) content: TimelineItemVideoContent) = ElementPreview { +internal fun TimelineItemVideoViewPreview(@PreviewParameter(TimelineItemVideoContentPreviewParam::class) content: TimelineItemVideoContent) = ElementPreview { TimelineItemVideoView( content = content, hideMediaContent = false, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt index 15387f51c0..a05ac78f84 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt @@ -42,7 +42,7 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContentProvider +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContentPreviewParam import io.element.android.libraries.designsystem.atomic.atoms.PlaybackSpeedButton import io.element.android.libraries.designsystem.components.media.WaveformPlaybackView import io.element.android.libraries.designsystem.preview.ElementPreview @@ -56,7 +56,7 @@ import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.utils.a11y.isTalkbackActive import io.element.android.libraries.voiceplayer.api.VoiceMessageEvent import io.element.android.libraries.voiceplayer.api.VoiceMessageState -import io.element.android.libraries.voiceplayer.api.VoiceMessageStateProvider +import io.element.android.libraries.voiceplayer.api.VoiceMessageStatePreviewParam import kotlinx.coroutines.delay @Composable @@ -261,9 +261,9 @@ private fun CustomIconButton( ) } -open class TimelineItemVoiceViewParametersProvider : PreviewParameterProvider { - private val voiceMessageStateProvider = VoiceMessageStateProvider() - private val timelineItemVoiceContentProvider = TimelineItemVoiceContentProvider() +open class TimelineItemVoiceViewParametersPreviewParam : PreviewParameterProvider { + private val voiceMessageStateProvider = VoiceMessageStatePreviewParam() + private val timelineItemVoiceContentProvider = TimelineItemVoiceContentPreviewParam() override val values: Sequence get() = timelineItemVoiceContentProvider.values.flatMap { content -> voiceMessageStateProvider.values.map { state -> @@ -283,7 +283,7 @@ data class TimelineItemVoiceViewParameters( @PreviewsDayNight @Composable internal fun TimelineItemVoiceViewPreview( - @PreviewParameter(TimelineItemVoiceViewParametersProvider::class) timelineItemVoiceViewParameters: TimelineItemVoiceViewParameters, + @PreviewParameter(TimelineItemVoiceViewParametersPreviewParam::class) timelineItemVoiceViewParameters: TimelineItemVoiceViewParameters, ) = ElementPreview { TimelineItemVoiceView( state = timelineItemVoiceViewParameters.state, @@ -296,7 +296,7 @@ internal fun TimelineItemVoiceViewPreview( @PreviewsDayNight @Composable internal fun TimelineItemVoiceViewUnifiedPreview() = ElementPreview { - val timelineItemVoiceViewParametersProvider = TimelineItemVoiceViewParametersProvider() + val timelineItemVoiceViewParametersProvider = TimelineItemVoiceViewParametersPreviewParam() Column { timelineItemVoiceViewParametersProvider.values.forEach { TimelineItemVoiceView( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryStatePreviewParam.kt similarity index 91% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryStateProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryStatePreviewParam.kt index 82974c8908..a617394723 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryStatePreviewParam.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.libraries.matrix.api.core.EventId -open class ReactionSummaryStateProvider : PreviewParameterProvider { +open class ReactionSummaryStatePreviewParam : PreviewParameterProvider { override val values = sequenceOf(aReactionSummaryState()) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt index 2a902fd692..5b6c7fda37 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/reactionsummary/ReactionSummaryView.kt @@ -293,7 +293,7 @@ private fun SenderRow( @PreviewsDayNight @Composable internal fun ReactionSummaryViewContentPreview( - @PreviewParameter(ReactionSummaryStateProvider::class) state: ReactionSummaryState + @PreviewParameter(ReactionSummaryStatePreviewParam::class) state: ReactionSummaryState ) = ElementPreview { ReactionSummaryViewContent(summary = state.target as ReactionSummaryState.Summary) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateForTimelineItemEventRowProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateForTimelineItemEventRowPreviewParam.kt similarity index 95% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateForTimelineItemEventRowProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateForTimelineItemEventRowPreviewParam.kt index c5140c4c03..a3556a9b03 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateForTimelineItemEventRowProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateForTimelineItemEventRowPreviewParam.kt @@ -13,7 +13,7 @@ import io.element.android.features.messages.impl.timeline.model.ReadReceiptData import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState -class ReadReceiptViewStateForTimelineItemEventRowProvider : +class ReadReceiptViewStateForTimelineItemEventRowPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStatePreviewParam.kt similarity index 96% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStatePreviewParam.kt index 64a7cf34ff..751a88cb39 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/ReadReceiptViewStatePreviewParam.kt @@ -17,7 +17,7 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import kotlinx.collections.immutable.toImmutableList -class ReadReceiptViewStateProvider : PreviewParameterProvider { +class ReadReceiptViewStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aReadReceiptViewState(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt index c9b712eba7..17a9b0c5eb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/TimelineItemReadReceiptView.kt @@ -202,7 +202,7 @@ private fun computeReceiptDescription(receipts: ImmutableList): @PreviewsDayNight @Composable internal fun TimelineItemReadReceiptViewPreview( - @PreviewParameter(ReadReceiptViewStateProvider::class) state: ReadReceiptViewState, + @PreviewParameter(ReadReceiptViewStatePreviewParam::class) state: ReadReceiptViewState, ) = ElementPreview { TimelineItemReadReceiptView( state = state, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt index 407cd29026..ad30a3765e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheet.kt @@ -117,7 +117,7 @@ private fun ReadReceiptBottomSheetContent( @PreviewsDayNight @Composable -internal fun ReadReceiptBottomSheetPreview(@PreviewParameter(ReadReceiptBottomSheetStateProvider::class) state: ReadReceiptBottomSheetState) { +internal fun ReadReceiptBottomSheetPreview(@PreviewParameter(ReadReceiptBottomSheetStatePreviewParam::class) state: ReadReceiptBottomSheetState) { ElementPreview(fillMaxSize = true) { ReadReceiptBottomSheet( state = state, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetStatePreviewParam.kt similarity index 87% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetStateProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetStatePreviewParam.kt index ff297a4f8d..86c2beb707 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/receipt/bottomsheet/ReadReceiptBottomSheetStatePreviewParam.kt @@ -10,14 +10,14 @@ package io.element.android.features.messages.impl.timeline.components.receipt.bo import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.messages.impl.timeline.aTimelineItemEvent -import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewStateProvider +import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewStatePreviewParam import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import kotlinx.collections.immutable.toImmutableList -class ReadReceiptBottomSheetStateProvider : PreviewParameterProvider { - // Reuse the provider ReadReceiptViewStateProvider - private val readReceiptViewStateProvider = ReadReceiptViewStateProvider() +class ReadReceiptBottomSheetStatePreviewParam : PreviewParameterProvider { + // Reuse the provider ReadReceiptViewStatePreviewParam + private val readReceiptViewStateProvider = ReadReceiptViewStatePreviewParam() override val values: Sequence = readReceiptViewStateProvider.values .filter { it.sendState is LocalEventSendState.Sent } .map { readReceiptViewState -> diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemDaySeparatorView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemDaySeparatorView.kt index 30befea07d..f2a62aa4a4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemDaySeparatorView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineItemDaySeparatorView.kt @@ -21,7 +21,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.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel -import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModelProvider +import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModelPreviewParam import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text @@ -53,7 +53,7 @@ internal fun TimelineItemDaySeparatorView( @PreviewsDayNight @Composable internal fun TimelineItemDaySeparatorViewPreview( - @PreviewParameter(TimelineItemDaySeparatorModelProvider::class) model: TimelineItemDaySeparatorModel + @PreviewParameter(TimelineItemDaySeparatorModelPreviewParam::class) model: TimelineItemDaySeparatorModel ) = ElementPreview { TimelineItemDaySeparatorView( model = model, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactories.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactories.kt index 1063157bef..2234593c4b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactories.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactories.kt @@ -10,28 +10,13 @@ package io.element.android.features.messages.impl.timeline.di import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import dev.zacsweers.metro.BindingContainer -import dev.zacsweers.metro.ContributesTo import dev.zacsweers.metro.Inject -import dev.zacsweers.metro.Multibinds import dev.zacsweers.metro.SingleIn import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope import kotlin.reflect.KClass -/** - * Container that declares the [TimelineItemPresenterFactory] map multi binding. - * - * Its sole purpose is to support the case of an empty map multibinding. - */ -@BindingContainer -@ContributesTo(RoomScope::class) -interface TimelineItemPresenterFactoriesModule { - @Multibinds - fun multiBindTimelineItemPresenterFactories(): @JvmSuppressWildcards Map, TimelineItemPresenterFactory<*, *>> -} - /** * Room level caching layer for the [TimelineItemPresenterFactory] instances. * diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactoriesBindingContainer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactoriesBindingContainer.kt new file mode 100644 index 0000000000..65dcd6dee0 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactoriesBindingContainer.kt @@ -0,0 +1,27 @@ +/* + * 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.messages.impl.timeline.di + +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Multibinds +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent +import io.element.android.libraries.di.RoomScope +import kotlin.reflect.KClass + +/** + * Container that declares the [TimelineItemPresenterFactory] map multi binding. + * + * Its sole purpose is to support the case of an empty map multibinding. + */ +@BindingContainer +@ContributesTo(RoomScope::class) +interface TimelineItemPresenterFactoriesBindingContainer { + @Multibinds + fun multiBindTimelineItemPresenterFactories(): @JvmSuppressWildcards Map, TimelineItemPresenterFactory<*, *>> +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt index 1679a6030a..9bf7aa3c32 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt @@ -21,6 +21,9 @@ import io.element.android.libraries.androidutils.diff.MutableListDiffCache import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent +import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -110,7 +113,11 @@ class TimelineItemsFactory( ): TimelineItem? { val timelineItem = when (val currentTimelineItem = timelineItems[index]) { - is MatrixTimelineItem.Event -> eventItemFactory.create(currentTimelineItem, index, timelineItems, roomMembers, renderReadReceipts) + is MatrixTimelineItem.Event -> if (currentTimelineItem.event.isKeyVerificationRequest()) { + null + } else { + eventItemFactory.create(currentTimelineItem, index, timelineItems, roomMembers, renderReadReceipts) + } is MatrixTimelineItem.Virtual -> virtualItemFactory.create(currentTimelineItem) MatrixTimelineItem.Other -> null } @@ -118,3 +125,8 @@ class TimelineItemsFactory( return timelineItem } } + +private fun EventTimelineItem.isKeyVerificationRequest(): Boolean { + val messageType = (content as? MessageContent)?.type + return messageType is OtherMessageType && messageType.isKeyVerificationRequest +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStatePreviewParam.kt similarity index 93% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStatePreviewParam.kt index 0a6fb70cc5..c28e9c4d92 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStatePreviewParam.kt @@ -13,7 +13,7 @@ import io.element.android.features.messages.impl.timeline.FocusRequestState import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.room.errors.FocusEventException -open class FocusRequestStateProvider : PreviewParameterProvider { +open class FocusRequestStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( FocusRequestState.Loading( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateView.kt index 3dff7ac4f7..2bda2fe4bf 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/focus/FocusRequestStateView.kt @@ -55,7 +55,7 @@ fun FocusRequestStateView( @PreviewsDayNight @Composable internal fun FocusRequestStateViewPreview( - @PreviewParameter(FocusRequestStateProvider::class) state: FocusRequestState, + @PreviewParameter(FocusRequestStatePreviewParam::class) state: FocusRequestState, ) = ElementPreview { FocusRequestStateView( focusRequestState = state, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReactionProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReactionPreviewParam.kt similarity index 95% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReactionProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReactionPreviewParam.kt index e5eb52c91e..f7ede5ebd7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReactionProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/AggregatedReactionPreviewParam.kt @@ -15,7 +15,7 @@ import java.text.DateFormat import java.util.Date import java.util.TimeZone -open class AggregatedReactionProvider : PreviewParameterProvider { +open class AggregatedReactionPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf(false, true).flatMap { sequenceOf( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemReactionsProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemReactionsProvider.kt index 3190e61a75..35d4f21dda 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemReactionsProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItemReactionsProvider.kt @@ -11,6 +11,6 @@ package io.element.android.features.messages.impl.timeline.model import kotlinx.collections.immutable.toImmutableList fun aTimelineItemReactions() = TimelineItemReactions( - // Use values from AggregatedReactionProvider - reactions = AggregatedReactionProvider().values.toImmutableList() + // Use values from AggregatedReactionPreviewParam + reactions = AggregatedReactionPreviewParam().values.toImmutableList() ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleStatePreviewParam.kt similarity index 94% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleStateProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleStatePreviewParam.kt index cdd24ed264..4a7a9f8187 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleStatePreviewParam.kt @@ -13,7 +13,7 @@ import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition -open class BubbleStateProvider : PreviewParameterProvider { +open class BubbleStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( TimelineItemGroupPosition.First, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentPreviewParam.kt similarity index 92% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentPreviewParam.kt index 3b60a48c78..600288012b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentPreviewParam.kt @@ -13,7 +13,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.media.MediaSource import kotlin.time.Duration.Companion.milliseconds -open class TimelineItemAudioContentProvider : PreviewParameterProvider { +open class TimelineItemAudioContentPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aTimelineItemAudioContent("A sound.mp3"), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEncryptedContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEncryptedContentPreviewParam.kt similarity index 96% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEncryptedContentProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEncryptedContentPreviewParam.kt index bb53da3b7b..a85553360a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEncryptedContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEncryptedContentPreviewParam.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent import io.element.android.libraries.matrix.api.timeline.item.event.UtdCause -open class TimelineItemEncryptedContentProvider : PreviewParameterProvider { +open class TimelineItemEncryptedContentPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aTimelineItemEncryptedContent(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentPreviewParam.kt similarity index 96% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentPreviewParam.kt index 1d4c7c04ec..0dd91fccfe 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentPreviewParam.kt @@ -18,7 +18,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecry import kotlinx.collections.immutable.toImmutableList import org.jsoup.nodes.Document -class TimelineItemEventContentProvider : PreviewParameterProvider { +class TimelineItemEventContentPreviewParam : PreviewParameterProvider { override val values = sequenceOf( aTimelineItemEmoteContent(), aTimelineItemEncryptedContent(), @@ -45,7 +45,7 @@ class TimelineItemEventContentProvider : PreviewParameterProvider { +class TimelineItemTextBasedContentPreviewParam : PreviewParameterProvider { private fun buildSpanned(text: String) = buildSpannedString { inSpans(StyleSpan(Typeface.BOLD)) { append("Rich Text") diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContentPreviewParam.kt similarity index 92% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContentProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContentPreviewParam.kt index e2021e9646..277f461edc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContentPreviewParam.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.media.MediaSource -open class TimelineItemFileContentProvider : PreviewParameterProvider { +open class TimelineItemFileContentPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aTimelineItemFileContent(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentPreviewParam.kt similarity index 93% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentPreviewParam.kt index 6d3b114428..cd93c7c006 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentPreviewParam.kt @@ -13,7 +13,7 @@ import androidx.media3.common.MimeTypes import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH -open class TimelineItemImageContentProvider : PreviewParameterProvider { +open class TimelineItemImageContentPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aTimelineItemImageContent(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContentPreviewParam.kt similarity index 95% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContentProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContentPreviewParam.kt index 6c095c828e..e4f2691f27 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContentPreviewParam.kt @@ -14,7 +14,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails import io.element.android.libraries.matrix.ui.messages.reply.aProfileDetailsReady -open class TimelineItemLocationContentProvider : PreviewParameterProvider { +open class TimelineItemLocationContentPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aTimelineItemLocationContent( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentPreviewParam.kt similarity index 94% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentPreviewParam.kt index 26577f5230..2cabada8b9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentPreviewParam.kt @@ -15,7 +15,7 @@ import io.element.android.features.poll.api.pollcontent.aPollQuestion import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.PollKind -open class TimelineItemPollContentProvider : PreviewParameterProvider { +open class TimelineItemPollContentPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aTimelineItemPollContent(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStickerContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStickerContentPreviewParam.kt similarity index 92% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStickerContentProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStickerContentPreviewParam.kt index eb564cd803..def748d97b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStickerContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStickerContentPreviewParam.kt @@ -13,7 +13,7 @@ import androidx.media3.common.MimeTypes import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH -open class TimelineItemStickerContentProvider : PreviewParameterProvider { +open class TimelineItemStickerContentPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aTimelineItemStickerContent(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentPreviewParam.kt similarity index 94% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentPreviewParam.kt index 627dbf3dd7..46de9ea89a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentPreviewParam.kt @@ -14,7 +14,7 @@ import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH import kotlin.time.Duration.Companion.milliseconds -open class TimelineItemVideoContentProvider : PreviewParameterProvider { +open class TimelineItemVideoContentPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aTimelineItemVideoContent(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentPreviewParam.kt similarity index 95% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentPreviewParam.kt index d596963ebc..9b9a044791 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentPreviewParam.kt @@ -17,7 +17,7 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.minutes -open class TimelineItemVoiceContentProvider : PreviewParameterProvider { +open class TimelineItemVoiceContentPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aTimelineItemVoiceContent( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemDaySeparatorModelProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemDaySeparatorModelPreviewParam.kt similarity index 85% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemDaySeparatorModelProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemDaySeparatorModelPreviewParam.kt index d31d35d62f..2fb4e7ac63 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemDaySeparatorModelProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemDaySeparatorModelPreviewParam.kt @@ -10,7 +10,7 @@ package io.element.android.features.messages.impl.timeline.model.virtual import androidx.compose.ui.tooling.preview.PreviewParameterProvider -class TimelineItemDaySeparatorModelProvider : PreviewParameterProvider { +class TimelineItemDaySeparatorModelPreviewParam : PreviewParameterProvider { override val values = sequenceOf( aTimelineItemDaySeparatorModel("Today"), aTimelineItemDaySeparatorModel("March 6, 2023") diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/AspectRatioProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/AspectRatioPreviewParam.kt similarity index 88% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/AspectRatioProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/AspectRatioPreviewParam.kt index 0ebd63439a..43118d96fd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/AspectRatioProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/AspectRatioPreviewParam.kt @@ -10,7 +10,7 @@ package io.element.android.features.messages.impl.timeline.protection import androidx.compose.ui.tooling.preview.PreviewParameterProvider -class AspectRatioProvider : PreviewParameterProvider { +class AspectRatioPreviewParam : PreviewParameterProvider { override val values: Sequence = sequenceOf( null, 0.05f, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedView.kt index 2dbe47e9e4..f8a0b7ac89 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/protection/ProtectedView.kt @@ -83,7 +83,7 @@ fun ProtectedView( @PreviewsDayNight @Composable internal fun ProtectedViewPreview( - @PreviewParameter(AspectRatioProvider::class) aspectRatio: Float?, + @PreviewParameter(AspectRatioPreviewParam::class) aspectRatio: Float?, ) = ElementPreview { TimelineItemAspectRatioBox( modifier = Modifier.blurHashBackground(A_BLUR_HASH, alpha = 0.9f), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogState.kt new file mode 100644 index 0000000000..02da5a6503 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogState.kt @@ -0,0 +1,30 @@ +/* + * 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.messages.impl.timeline.sendfailure + +import androidx.compose.runtime.Immutable +import io.element.android.features.messages.impl.timeline.model.TimelineItem + +@Immutable +sealed interface SendFailureDialogState { + data object Hidden : SendFailureDialogState + + data class Show( + val event: TimelineItem.Event, + val sendFailureType: SendFailureType, + ) : SendFailureDialogState + + @Immutable + sealed interface SendFailureType { + data class InvalidMimeType(val mimeType: String) : SendFailureType + data object MissingMediaContent : SendFailureType + data object SendingFromUnverifiedDevice : SendFailureType + data class Error(val message: String) : SendFailureType + data object Unknown : SendFailureType + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogStatePreviewParam.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogStatePreviewParam.kt new file mode 100644 index 0000000000..728431cc44 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogStatePreviewParam.kt @@ -0,0 +1,38 @@ +/* + * 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.messages.impl.timeline.sendfailure + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.messages.impl.timeline.aTimelineItemEvent + +class SendFailureDialogStatePreviewParam : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + SendFailureDialogState.Hidden, + SendFailureDialogState.Show( + event = aTimelineItemEvent(), + sendFailureType = SendFailureDialogState.SendFailureType.InvalidMimeType("image/invalid") + ), + SendFailureDialogState.Show( + event = aTimelineItemEvent(), + sendFailureType = SendFailureDialogState.SendFailureType.MissingMediaContent, + ), + SendFailureDialogState.Show( + event = aTimelineItemEvent(), + sendFailureType = SendFailureDialogState.SendFailureType.SendingFromUnverifiedDevice, + ), + SendFailureDialogState.Show( + event = aTimelineItemEvent(), + sendFailureType = SendFailureDialogState.SendFailureType.Error("Message error from server"), + ), + SendFailureDialogState.Show( + event = aTimelineItemEvent(), + sendFailureType = SendFailureDialogState.SendFailureType.Unknown, + ) + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogView.kt new file mode 100644 index 0000000000..15ee42d575 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogView.kt @@ -0,0 +1,79 @@ +/* + * 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.messages.impl.timeline.sendfailure + +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.messages.impl.R +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun SendFailureDialogView( + sendFailureDialogState: SendFailureDialogState, + onDismiss: () -> Unit, + onRetry: (TimelineItem.Event) -> Unit, + onRemoveMessage: (TimelineItem.Event) -> Unit, + modifier: Modifier = Modifier, +) { + when (sendFailureDialogState) { + is SendFailureDialogState.Hidden -> Unit + is SendFailureDialogState.Show -> { + // Show the dialog with the message and actions + val content = when (sendFailureDialogState.sendFailureType) { + is SendFailureDialogState.SendFailureType.InvalidMimeType -> stringResource( + id = R.string.error_sending_failure_invalid_mime_type, + sendFailureDialogState.sendFailureType.mimeType, + ) + is SendFailureDialogState.SendFailureType.MissingMediaContent -> stringResource( + id = R.string.error_sending_failure_missing_media_content, + ) + is SendFailureDialogState.SendFailureType.SendingFromUnverifiedDevice -> stringResource( + id = R.string.error_sending_failure_sending_from_unverified_device, + ) + is SendFailureDialogState.SendFailureType.Error -> stringResource( + id = R.string.error_sending_failure_other, + sendFailureDialogState.sendFailureType.message + ) + is SendFailureDialogState.SendFailureType.Unknown -> stringResource( + id = R.string.error_sending_failure_unknown, + ) + } + + ConfirmationDialog( + modifier = modifier, + title = stringResource(id = CommonStrings.common_sending_failed), + content = content, + onDismiss = onDismiss, + submitText = stringResource(id = CommonStrings.action_retry), + onSubmitClick = { onRetry(sendFailureDialogState.event) }, + onCancelClick = onDismiss, + thirdButtonText = stringResource(id = CommonStrings.action_remove_message), + onThirdButtonClick = { onRemoveMessage(sendFailureDialogState.event) }, + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun SendFailureDialogViewPreview( + @PreviewParameter(SendFailureDialogStatePreviewParam::class) sendFailureDialogState: SendFailureDialogState, +) = ElementPreview { + SendFailureDialogView( + sendFailureDialogState = sendFailureDialogState, + onDismiss = {}, + onRetry = {}, + onRemoveMessage = {}, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt index 6aabf97f2c..d6a55c0563 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/topbars/MessagesViewTopBar.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing @@ -84,6 +85,7 @@ internal fun MessagesViewTopBar( val roundedCornerShape = RoundedCornerShape(8.dp) Row( modifier = Modifier + .heightIn(min = 48.dp) .clip(roundedCornerShape) .clickable { onRoomDetailsClick() } .semantics { heading() }, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStatePreviewParam.kt similarity index 97% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStatePreviewParam.kt index e298b3af26..b98262a2a8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationStatePreviewParam.kt @@ -16,7 +16,7 @@ import io.element.android.libraries.designsystem.preview.USER_NAME_DAVID import io.element.android.libraries.designsystem.preview.USER_NAME_EVE import kotlinx.collections.immutable.toImmutableList -class TypingNotificationStateProvider : PreviewParameterProvider { +class TypingNotificationStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aTypingNotificationState(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt index c8e5722b19..3d2227c6d6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/TypingNotificationView.kt @@ -141,7 +141,7 @@ private fun computeTypingNotificationText(typingMembers: ImmutableList -} - @AssistedInject class VoiceMessagePresenter( voiceMessagePresenterFactory: VoiceMessagePresenterFactory, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/di/VoiceMessagePresenterBindingContainer.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/di/VoiceMessagePresenterBindingContainer.kt new file mode 100644 index 0000000000..422ade8856 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/di/VoiceMessagePresenterBindingContainer.kt @@ -0,0 +1,27 @@ +/* + * 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.messages.impl.voicemessages.timeline.di + +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.IntoMap +import io.element.android.features.messages.impl.timeline.di.TimelineItemEventContentKey +import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactory +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent +import io.element.android.features.messages.impl.voicemessages.timeline.VoiceMessagePresenter +import io.element.android.libraries.di.RoomScope + +@BindingContainer +@ContributesTo(RoomScope::class) +interface VoiceMessagePresenterBindingContainer { + @Binds + @IntoMap + @TimelineItemEventContentKey(TimelineItemVoiceContent::class) + fun bindVoiceMessagePresenterFactory(factory: VoiceMessagePresenter.Factory): TimelineItemPresenterFactory<*, *> +} diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index 556ab00ee0..e29b83af41 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -7,6 +7,11 @@ "Encrypted by an unknown or deleted device." "Encrypted by a device not verified by its owner." "Encrypted by an unverified user." + "Invalid mime type: %s" + "Missing media content" + "Error details: %s" + "Sending from unverified device" + "Unknown error" "Flip image horizontally" "Flipped horizontally" "Original" diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 0a0d310b16..227088f78c 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -72,6 +72,7 @@ import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EVENT_ID @@ -83,6 +84,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID_2 import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.core.FakeSendHandle import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser @@ -1043,6 +1045,56 @@ class MessagesPresenterTest { } } + @Test + fun `present - handle action retry sending`() = runTest { + val retryLambda = lambdaRecorder> { Result.success(Unit) } + val messageEvent = aMessageEvent( + sendState = LocalEventSendState.Failed.Unknown("Error"), + sendHandleProvider = { FakeSendHandle(retryLambda = retryLambda) }, + ) + val presenter = createMessagesPresenter() + presenter.testWithLifecycleOwner { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.RetrySending, messageEvent)) + advanceUntilIdle() + retryLambda.assertions().isCalledOnce() + } + } + + @Test + fun `present - handle action retry sending - failure is ignored`() = runTest { + val retryLambda = lambdaRecorder> { Result.failure(AN_EXCEPTION) } + val messageEvent = aMessageEvent( + sendState = LocalEventSendState.Failed.Unknown("Error"), + sendHandleProvider = { FakeSendHandle(retryLambda = retryLambda) }, + ) + val presenter = createMessagesPresenter() + presenter.testWithLifecycleOwner { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.RetrySending, messageEvent)) + advanceUntilIdle() + retryLambda.assertions().isCalledOnce() + } + } + + @Test + fun `present - handle action retry sending - no send handle, it should have no effect`() = runTest { + val messageEvent = aMessageEvent( + sendState = LocalEventSendState.Failed.Unknown("Error"), + sendHandleProvider = { null }, + ) + val presenter = createMessagesPresenter() + presenter.testWithLifecycleOwner { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.RetrySending, messageEvent)) + advanceUntilIdle() + // No op! + } + } + @Test fun `present - handle action view in timeline, it should have no effect`() = runTest { val messageEvent = aMessageEvent( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index 2800c737ad..f662da0eb0 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -55,12 +55,14 @@ import io.element.android.features.messages.impl.timeline.components.receipt.aRe import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvent import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent +import io.element.android.features.messages.impl.timeline.sendfailure.SendFailureDialogState import io.element.android.features.roomcall.api.aStandByCallState import io.element.android.libraries.emoji.impl.picker.DefaultEmojiPickerRenderer import io.element.android.libraries.emoji.impl.picker.anEmojiPickerState 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.room.tombstone.SuccessorRoom +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName import io.element.android.libraries.matrix.api.user.MatrixUser @@ -88,6 +90,7 @@ import org.junit.Test import org.robolectric.annotation.Config import kotlin.time.Duration.Companion.milliseconds +@Suppress("LargeClass") class MessagesViewTest : RobolectricTest() { @Test fun `clicking on back invoke expected callback`() = runAndroidComposeUiTest { @@ -176,6 +179,33 @@ class MessagesViewTest : RobolectricTest() { callback.assertSuccess() } + @Test + fun `clicking on an Event which has not been sent yet invoke expected callback`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder(expectEvents = false) + val localEcho = aTimelineItemEvent( + isMine = true, + content = aTimelineItemTextContent(), + ).copy(eventId = null) + val state = aMessagesState( + timelineState = aTimelineState( + timelineItems = persistentListOf(localEcho), + ), + eventSink = eventsRecorder + ) + val callback = EnsureCalledOnceWithTwoParamsAndResult( + expectedParam1 = true, + expectedParam2 = localEcho, + result = true, + ) + setMessagesView( + state = state, + onEventClick = callback, + ) + // Cannot perform click on "Text", it's not detected. Use tag instead + onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performClick() + callback.assertSuccess() + } + @Test fun `long clicking on an Event emits the expected Event userHasPermissionToSendMessage`() { `long clicking on an Event emits the expected Event`(userHasPermissionToSendMessage = true) @@ -531,6 +561,80 @@ class MessagesViewTest : RobolectricTest() { eventsRecorder.assertSingle(TimelineEvent.ComputeVerifiedUserSendFailure(timelineItem)) } + @Test + fun `clicking on retry in the send failure dialog emits the expected Events`() = runAndroidComposeUiTest { + val messagesEventsRecorder = EventsRecorder() + val timelineEventsRecorder = EventsRecorder() + val timelineItem = aTimelineItemEvent(sendState = LocalEventSendState.Failed.Unknown("Error")) + setMessagesView( + state = aMessagesState( + timelineState = aTimelineState( + sendFailureDialogState = SendFailureDialogState.Show( + event = timelineItem, + sendFailureType = SendFailureDialogState.SendFailureType.Error("Error"), + ), + eventSink = timelineEventsRecorder, + ), + eventSink = messagesEventsRecorder, + ), + ) + // Clear initial 'LoadMore' event emitted when setting the state + timelineEventsRecorder.clear() + + clickOn(CommonStrings.action_retry) + messagesEventsRecorder.assertSingle(MessagesEvent.HandleAction(TimelineItemAction.RetrySending, timelineItem)) + timelineEventsRecorder.assertSingle(TimelineEvent.HideSendFailureDialog) + } + + @Test + fun `clicking on remove message in the send failure dialog emits the expected Events`() = runAndroidComposeUiTest { + val messagesEventsRecorder = EventsRecorder() + val timelineEventsRecorder = EventsRecorder() + val timelineItem = aTimelineItemEvent(sendState = LocalEventSendState.Failed.Unknown("Error")) + setMessagesView( + state = aMessagesState( + timelineState = aTimelineState( + sendFailureDialogState = SendFailureDialogState.Show( + event = timelineItem, + sendFailureType = SendFailureDialogState.SendFailureType.Error("Error"), + ), + eventSink = timelineEventsRecorder, + ), + eventSink = messagesEventsRecorder, + ), + ) + // Clear initial 'LoadMore' event emitted when setting the state + timelineEventsRecorder.clear() + + clickOn(CommonStrings.action_remove_message) + messagesEventsRecorder.assertSingle(MessagesEvent.HandleAction(TimelineItemAction.Redact, timelineItem)) + timelineEventsRecorder.assertSingle(TimelineEvent.HideSendFailureDialog) + } + + @Test + fun `cancelling the send failure dialog emits the expected Event`() = runAndroidComposeUiTest { + val messagesEventsRecorder = EventsRecorder(expectEvents = false) + val timelineEventsRecorder = EventsRecorder() + val timelineItem = aTimelineItemEvent(sendState = LocalEventSendState.Failed.Unknown("Error")) + setMessagesView( + state = aMessagesState( + timelineState = aTimelineState( + sendFailureDialogState = SendFailureDialogState.Show( + event = timelineItem, + sendFailureType = SendFailureDialogState.SendFailureType.Error("Error"), + ), + eventSink = timelineEventsRecorder, + ), + eventSink = messagesEventsRecorder, + ), + ) + // Clear initial 'LoadMore' event emitted when setting the state + timelineEventsRecorder.clear() + + clickOn(CommonStrings.action_cancel) + timelineEventsRecorder.assertSingle(TimelineEvent.HideSendFailureDialog) + } + @Test fun `clicking on a custom emoji emits the expected Events`() = runAndroidComposeUiTest { val aUnicode = "🙈" diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt index 07b9a4e330..4170ae41ab 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt @@ -1056,7 +1056,8 @@ class MessageComposerPresenterTest : RobolectricTest() { baseRoom = FakeBaseRoom( roomPermissions = FakeRoomPermissions( canTriggerRoomNotification = true, - ) + ), + updateMembersResult = {}, ), typingNoticeResult = { Result.success(Unit) } ).apply { @@ -1103,6 +1104,55 @@ class MessageComposerPresenterTest : RobolectricTest() { } } + @Test + fun `present - mention suggestions refresh the room members`() = runTest { + val currentUser = aRoomMember(userId = A_USER_ID, membership = RoomMembershipState.JOIN) + val bob = aRoomMember(userId = A_USER_ID_2, membership = RoomMembershipState.JOIN) + val updateMembersResult = lambdaRecorder { } + val room = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + roomPermissions = FakeRoomPermissions( + canTriggerRoomNotification = true, + ), + updateMembersResult = updateMembersResult, + ), + typingNoticeResult = { Result.success(Unit) } + ).apply { + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf(currentUser, bob), + ) + ) + givenRoomInfo(aRoomInfo(isDirect = false)) + } + val presenter = createPresenter( + room = room, + slashCommandService = FakeSlashCommandService( + getSuggestionsResult = { _, _ -> emptyList() }, + ), + ) + presenter.test { + val initialState = awaitItem() + initialState.eventSink(MessageComposerEvent.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, ""))) + skipItems(1) + assertThat(awaitItem().suggestions).containsExactly(ResolvedSuggestion.AtRoom, ResolvedSuggestion.Member(bob)) + updateMembersResult.assertions().isCalledOnce() + + // Typing after the `@` must not refresh the members again + initialState.eventSink(MessageComposerEvent.SuggestionReceived(Suggestion(0, 1, SuggestionType.Mention, "b"))) + initialState.eventSink(MessageComposerEvent.SuggestionReceived(Suggestion(0, 2, SuggestionType.Mention, "bo"))) + advanceUntilIdle() + updateMembersResult.assertions().isCalledOnce() + + // Starting a new mention refreshes them again + initialState.eventSink(MessageComposerEvent.SuggestionReceived(null)) + initialState.eventSink(MessageComposerEvent.SuggestionReceived(Suggestion(0, 0, SuggestionType.Mention, ""))) + advanceUntilIdle() + updateMembersResult.assertions().isCalledExactly(2) + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `present - room mention suggestions no permission`() = runTest { val currentUser = aRoomMember(userId = A_USER_ID, membership = RoomMembershipState.JOIN) @@ -1113,7 +1163,8 @@ class MessageComposerPresenterTest : RobolectricTest() { baseRoom = FakeBaseRoom( roomPermissions = FakeRoomPermissions( canTriggerRoomNotification = false, - ) + ), + updateMembersResult = {}, ), typingNoticeResult = { Result.success(Unit) } ).apply { @@ -1144,6 +1195,7 @@ class MessageComposerPresenterTest : RobolectricTest() { val room = FakeJoinedRoom( baseRoom = FakeBaseRoom( roomPermissions = FakeRoomPermissions(canTriggerRoomNotification = true), + updateMembersResult = {}, ), typingNoticeResult = { Result.success(Unit) } ).apply { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt index 3efa707c3d..cafb16c41f 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenterTest.kt @@ -10,6 +10,8 @@ package io.element.android.features.messages.impl.pinned.list import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.PinUnpinAction +import io.element.android.features.messages.impl.aCustomReactionState +import io.element.android.features.messages.impl.aReactionSummaryState import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator @@ -400,6 +402,8 @@ class PinnedMessagesListPresenterTest { snackbarDispatcher = SnackbarDispatcher(), actionListPresenter = { anActionListState() }, linkPresenter = { aLinkState() }, + customReactionPresenter = { aCustomReactionState() }, + reactionSummaryPresenter = { aReactionSummaryState() }, analyticsService = analyticsService, featureFlagService = featureFlagService, sessionCoroutineScope = this, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt index 35e60684a9..856f74e33a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt @@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent +import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam @@ -111,6 +112,7 @@ private fun AndroidComposeUiTest.setPinnedMessagesListView( onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, onLinkLongClick = onLinkLongClick, + emojiPickerRenderer = NoOpEmojiPickerRenderer, ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/threads/ThreadsListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/threads/ThreadsListPresenterTest.kt index 9f8d210a16..fab7602906 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/threads/ThreadsListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/threads/ThreadsListPresenterTest.kt @@ -10,7 +10,7 @@ package io.element.android.features.messages.impl.threads import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.fixtures.aTimelineItemContentFactory import io.element.android.features.messages.impl.messagesummary.FakeMessageSummaryFormatter -import io.element.android.features.messages.impl.threads.list.ThreadsListEvents +import io.element.android.features.messages.impl.threads.list.ThreadsListEvent import io.element.android.features.messages.impl.threads.list.ThreadsListPresenter import io.element.android.features.messages.impl.threads.list.aThreadListItem import io.element.android.libraries.dateformatter.test.FakeDateFormatter @@ -48,7 +48,7 @@ class ThreadsListPresenterTest { // Pagination is automatically triggered on start, so we should have one call to paginate already paginateRecorder.assertions().isCalledOnce() - initialItem.eventSink(ThreadsListEvents.Paginate) + initialItem.eventSink(ThreadsListEvent.Paginate) // Simulate a pagination result threadsListService.emit(listOf(aThreadListItem())) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index 3b0794305f..e00cec3322 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -12,6 +12,8 @@ import app.cash.turbine.ReceiveTurbine import com.google.common.truth.Truth.assertThat import io.element.android.features.location.test.FakeActiveLiveLocationShareManager import io.element.android.features.messages.impl.FakeMessagesNavigator +import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureEvent +import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState import io.element.android.features.messages.impl.crypto.sendfailure.resolve.aResolveVerifiedUserSendFailureState import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator @@ -21,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState +import io.element.android.features.messages.impl.timeline.sendfailure.SendFailureDialogState import io.element.android.features.messages.impl.typing.aTypingNotificationState import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager @@ -45,6 +48,7 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender import io.element.android.libraries.matrix.api.timeline.item.event.Receipt import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent @@ -1059,6 +1063,169 @@ class TimelinePresenterTest { } } + @Test + fun `present - a forward pagination while the focused event is not rendered yet is ignored`() = runTest { + val paginateLambda = lambdaRecorder(ensureNeverCalled = true) { _: Timeline.PaginationDirection -> + Result.success(true) + } + val room = aRoomWithDetachedTimeline(paginateLambda = paginateLambda) + val presenter = createTimelinePresenter( + room = room, + timeline = room.liveTimeline, + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink.invoke(TimelineEvent.FocusOnEvent(AN_EVENT_ID)) + val focusedState = consumeItemsUntilPredicate { it.focusRequestState == FocusRequestState.Success(AN_EVENT_ID) }.last() + assertThat(focusedState.isLive).isFalse() + initialState.eventSink.invoke(TimelineEvent.LoadMore(Timeline.PaginationDirection.FORWARDS)) + advanceUntilIdle() + assert(paginateLambda).isNeverCalled() + assertThat(expectMostRecentItem().isLive).isFalse() + } + } + + @Test + fun `present - a forward pagination is performed once the focused event has been rendered`() = runTest { + val paginateLambda = lambdaRecorder { _: Timeline.PaginationDirection -> + Result.success(true) + } + val room = aRoomWithDetachedTimeline(paginateLambda = paginateLambda) + val presenter = createTimelinePresenter( + room = room, + timeline = room.liveTimeline, + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink.invoke(TimelineEvent.FocusOnEvent(AN_EVENT_ID)) + consumeItemsUntilPredicate { it.focusRequestState == FocusRequestState.Success(AN_EVENT_ID) } + initialState.eventSink.invoke(TimelineEvent.OnFocusEventRender) + initialState.eventSink.invoke(TimelineEvent.LoadMore(Timeline.PaginationDirection.FORWARDS)) + advanceUntilIdle() + assert(paginateLambda) + .isCalledOnce() + .with(value(Timeline.PaginationDirection.FORWARDS)) + assertThat(expectMostRecentItem().isLive).isTrue() + } + } + + @Test + fun `present - a backward pagination while the focused event is not rendered yet is performed`() = runTest { + val paginateLambda = lambdaRecorder { _: Timeline.PaginationDirection -> + Result.success(false) + } + val room = aRoomWithDetachedTimeline(paginateLambda = paginateLambda) + val presenter = createTimelinePresenter( + room = room, + timeline = room.liveTimeline, + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink.invoke(TimelineEvent.FocusOnEvent(AN_EVENT_ID)) + consumeItemsUntilPredicate { it.focusRequestState == FocusRequestState.Success(AN_EVENT_ID) } + initialState.eventSink.invoke(TimelineEvent.LoadMore(Timeline.PaginationDirection.BACKWARDS)) + advanceUntilIdle() + assert(paginateLambda) + .isCalledOnce() + .with(value(Timeline.PaginationDirection.BACKWARDS)) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - a forward pagination while the focus request is debounced is ignored`() = runTest { + val paginateLambda = lambdaRecorder(ensureNeverCalled = true) { _: Timeline.PaginationDirection -> + Result.success(true) + } + val room = aRoomWithDetachedTimeline(paginateLambda = paginateLambda) + val presenter = createTimelinePresenter( + room = room, + timeline = room.liveTimeline, + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink.invoke(TimelineEvent.FocusOnEvent(AN_EVENT_ID, debounce = 1.seconds)) + // The focus request has started and is now waiting for the debounce to elapse + consumeItemsUntilPredicate { it.focusRequestState is FocusRequestState.Requested } + initialState.eventSink.invoke(TimelineEvent.LoadMore(Timeline.PaginationDirection.FORWARDS)) + advanceUntilIdle() + assert(paginateLambda).isNeverCalled() + assertThat(expectMostRecentItem().isLive).isFalse() + } + } + + @Test + fun `present - a forward pagination while the focused timeline is loading is ignored`() = runTest { + val paginateLambda = lambdaRecorder(ensureNeverCalled = true) { _: Timeline.PaginationDirection -> + Result.success(true) + } + val room = aRoomWithDetachedTimeline(paginateLambda = paginateLambda) + val presenter = createTimelinePresenter( + room = room, + timeline = room.liveTimeline, + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink.invoke(TimelineEvent.FocusOnEvent(AN_EVENT_ID)) + // The focused timeline is being created, the focus request has not resolved yet + consumeItemsUntilPredicate { it.focusRequestState is FocusRequestState.Loading } + initialState.eventSink.invoke(TimelineEvent.LoadMore(Timeline.PaginationDirection.FORWARDS)) + advanceUntilIdle() + assert(paginateLambda).isNeverCalled() + assertThat(expectMostRecentItem().isLive).isFalse() + } + } + + @Test + fun `present - a forward pagination after a failed focus request is performed`() = runTest { + val paginateLambda = lambdaRecorder { _: Timeline.PaginationDirection -> + Result.success(false) + } + val room = aRoomWithDetachedTimeline( + paginateLambda = paginateLambda, + createTimelineResult = { Result.failure(RuntimeException("An error")) }, + ) + val presenter = createTimelinePresenter( + room = room, + timeline = room.liveTimeline, + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink.invoke(TimelineEvent.FocusOnEvent(AN_EVENT_ID)) + consumeItemsUntilPredicate { it.focusRequestState is FocusRequestState.Failure } + initialState.eventSink.invoke(TimelineEvent.LoadMore(Timeline.PaginationDirection.FORWARDS)) + advanceUntilIdle() + assert(paginateLambda) + .isCalledOnce() + .with(value(Timeline.PaginationDirection.FORWARDS)) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - a forward pagination in a thread timeline is ignored`() = runTest { + val paginateLambda = lambdaRecorder { _: Timeline.PaginationDirection -> + Result.success(true) + } + val timeline = FakeTimeline(mode = Timeline.Mode.Thread(A_THREAD_ID)).apply { + this.paginateLambda = paginateLambda + } + val presenter = createTimelinePresenter(timeline = timeline) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink.invoke(TimelineEvent.LoadMore(Timeline.PaginationDirection.FORWARDS)) + advanceUntilIdle() + assert(paginateLambda).isNeverCalled() + // A backward pagination is still performed in a thread timeline + initialState.eventSink.invoke(TimelineEvent.LoadMore(Timeline.PaginationDirection.BACKWARDS)) + advanceUntilIdle() + assert(paginateLambda) + .isCalledOnce() + .with(value(Timeline.PaginationDirection.BACKWARDS)) + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `present - focus on known event retrieves the event from cache`() = runTest { val timelineItemIndexer = TimelineItemIndexer() @@ -1390,6 +1557,112 @@ class TimelinePresenterTest { } } + @Test + fun `present - show and hide send failure dialog`() = runTest { + val presenter = createTimelinePresenter() + val event = aMessageEvent(sendState = LocalEventSendState.Failed.Unknown("An error")) + presenter.test { + val initialState = awaitFirstItem() + assertThat(initialState.sendFailureDialogState).isEqualTo(SendFailureDialogState.Hidden) + initialState.eventSink(TimelineEvent.ShowSendFailureDialog(event)) + awaitItem().also { state -> + assertThat(state.sendFailureDialogState).isEqualTo( + SendFailureDialogState.Show( + event = event, + sendFailureType = SendFailureDialogState.SendFailureType.Error("An error"), + ) + ) + state.eventSink(TimelineEvent.HideSendFailureDialog) + } + awaitItem().also { state -> + assertThat(state.sendFailureDialogState).isEqualTo(SendFailureDialogState.Hidden) + } + } + } + + @Test + fun `present - show send failure dialog - unknown error with a blank reason`() { + assertSendFailureType( + sendState = LocalEventSendState.Failed.Unknown(error = ""), + expectedSendFailureType = SendFailureDialogState.SendFailureType.Unknown, + ) + } + + @Test + fun `present - show send failure dialog - invalid mime type`() { + assertSendFailureType( + sendState = LocalEventSendState.Failed.InvalidMimeType(mimeType = "invalid/mimeType"), + expectedSendFailureType = SendFailureDialogState.SendFailureType.InvalidMimeType("invalid/mimeType"), + ) + } + + @Test + fun `present - show send failure dialog - missing media content`() { + assertSendFailureType( + sendState = LocalEventSendState.Failed.MissingMediaContent, + expectedSendFailureType = SendFailureDialogState.SendFailureType.MissingMediaContent, + ) + } + + @Test + fun `present - show send failure dialog - sending from unverified device`() { + assertSendFailureType( + sendState = LocalEventSendState.Failed.SendingFromUnverifiedDevice, + expectedSendFailureType = SendFailureDialogState.SendFailureType.SendingFromUnverifiedDevice, + ) + } + + private fun assertSendFailureType( + sendState: LocalEventSendState.Failed, + expectedSendFailureType: SendFailureDialogState.SendFailureType, + ) = runTest { + val presenter = createTimelinePresenter() + val event = aMessageEvent(sendState = sendState) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink(TimelineEvent.ShowSendFailureDialog(event)) + assertThat(awaitItem().sendFailureDialogState).isEqualTo( + SendFailureDialogState.Show(event = event, sendFailureType = expectedSendFailureType) + ) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - show send failure dialog - verified user failure is delegated to the resolve presenter`() = runTest { + val resolveEventSinkRecorder = lambdaRecorder { } + val presenter = createTimelinePresenter( + resolveVerifiedUserSendFailurePresenter = { + aResolveVerifiedUserSendFailureState(eventSink = resolveEventSinkRecorder) + }, + ) + val event = aMessageEvent( + sendState = LocalEventSendState.Failed.VerifiedUserChangedIdentity(users = listOf(A_USER_ID)), + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink(TimelineEvent.ShowSendFailureDialog(event)) + // The generic dialog is not displayed, the dedicated one is. + assertThat(consumeItemsUntilTimeout()).isEmpty() + assertThat(initialState.sendFailureDialogState).isEqualTo(SendFailureDialogState.Hidden) + resolveEventSinkRecorder.assertions() + .isCalledOnce() + .with(value(ResolveVerifiedUserSendFailureEvent.ComputeForMessage(event))) + } + } + + @Test + fun `present - show send failure dialog - no effect if the event did not fail to send`() = runTest { + val presenter = createTimelinePresenter() + val event = aMessageEvent(sendState = LocalEventSendState.Sending.Event) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink(TimelineEvent.ShowSendFailureDialog(event)) + assertThat(consumeItemsUntilTimeout()).isEmpty() + assertThat(initialState.sendFailureDialogState).isEqualTo(SendFailureDialogState.Hidden) + } + } + @Test fun `present - when room member info is loaded, read receipts info should be updated`() = runTest { val timeline = FakeTimeline( @@ -1552,6 +1825,37 @@ class TimelinePresenterTest { return awaitItem() } + private fun aRoomWithDetachedTimeline( + paginateLambda: (Timeline.PaginationDirection) -> Result, + createTimelineResult: () -> Result = { Result.success(aDetachedTimeline(paginateLambda)) }, + ): FakeJoinedRoom { + return FakeJoinedRoom( + liveTimeline = FakeTimeline(timelineItems = flowOf(emptyList())).apply { + this.paginateLambda = paginateLambda + }, + createTimelineResult = { createTimelineResult() }, + baseRoom = FakeBaseRoom( + roomPermissions = roomPermissions(), + threadRootIdForEventResult = { _ -> Result.success(null) }, + ), + ) + } + + private fun aDetachedTimeline( + paginateLambda: (Timeline.PaginationDirection) -> Result, + ) = FakeTimeline( + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Event( + uniqueId = A_UNIQUE_ID, + event = anEventTimelineItem(eventId = AN_EVENT_ID), + ) + ) + ) + ).apply { + this.paginateLambda = paginateLambda + } + private fun roomPermissions( canRedactOther: Boolean = false, canRedactOwn: Boolean = true, @@ -1589,6 +1893,7 @@ class TimelinePresenterTest { liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(), markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead { _, _ -> }, timelineProtectionPresenter: Presenter = { aTimelineProtectionState() }, + resolveVerifiedUserSendFailurePresenter: Presenter = { aResolveVerifiedUserSendFailureState() }, ): TimelinePresenter { return TimelinePresenter( timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), @@ -1602,7 +1907,7 @@ class TimelinePresenterTest { sessionPreferencesStore = sessionPreferencesStore, timelineItemIndexer = timelineItemIndexer, timelineController = TimelineController(room, timeline), - resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() }, + resolveVerifiedUserSendFailurePresenter = resolveVerifiedUserSendFailurePresenter, typingNotificationPresenter = { aTypingNotificationState() }, roomCallStatePresenter = { aStandByCallState() }, featureFlagService = featureFlagService, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index f733f116d6..ac84d48c95 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -33,6 +33,7 @@ import io.element.android.features.messages.impl.timeline.protection.aTimelinePr import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType @@ -178,6 +179,46 @@ class TimelineViewTest : RobolectricTest() { eventsRecorder.assertSingle(TimelineEvent.HideShieldDialog) } + @Test + fun `clicking on the send failure indicator emits the expected Event`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + val timelineItem = aTimelineItemEvent( + content = aTimelineItemTextContent(), + sendState = LocalEventSendState.Failed.Unknown("Error"), + ) + setTimelineView( + state = aTimelineState( + timelineItems = persistentListOf(timelineItem), + eventSink = eventsRecorder, + ), + ) + eventsRecorder.clear() + + val contentDescription = activity!!.getString(CommonStrings.common_sending_failed) + onNodeWithContentDescription(contentDescription).performClick() + eventsRecorder.assertSingle(TimelineEvent.ShowSendFailureDialog(timelineItem)) + } + + @Test + fun `clicking on the send failure indicator of a verified user failure emits the expected Event`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + val timelineItem = aTimelineItemEvent( + content = aTimelineItemTextContent(), + sendState = LocalEventSendState.Failed.VerifiedUserChangedIdentity(users = listOf(A_USER_ID)), + ) + setTimelineView( + state = aTimelineState( + timelineItems = persistentListOf(timelineItem), + eventSink = eventsRecorder, + ), + ) + eventsRecorder.clear() + + val contentDescription = activity!!.getString(CommonStrings.common_sending_failed) + onNodeWithContentDescription(contentDescription).performClick() + eventsRecorder.assertSingle(TimelineEvent.ShowSendFailureDialog(timelineItem)) + } + @Test fun `clicking on a reply to a loaded event emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactoryTest.kt index 53c39ee530..f93c8f196e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactoryTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactoryTest.kt @@ -12,8 +12,10 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem @@ -82,6 +84,50 @@ class TimelineItemsFactoryTest { ).inOrder() } + @Test + fun `a key verification request is not rendered`() = runTest { + val factory = aTimelineItemsFactory( + config = TimelineItemsFactoryConfig( + computeReadReceipts = false, + computeReactions = false, + ) + ) + val items = listOf( + MatrixTimelineItem.Event( + uniqueId = UniqueId("event-0"), + event = anEventTimelineItem( + sender = A_USER_ID, + content = aMessageContent( + body = "Alice is requesting to verify your key, but your client does not support in-chat key verification.", + messageType = OtherMessageType( + msgType = "m.key.verification.request", + body = "Alice is requesting to verify your key, but your client does not support in-chat key verification.", + ), + ), + ), + ), + MatrixTimelineItem.Event( + uniqueId = UniqueId("event-1"), + event = anEventTimelineItem( + sender = A_USER_ID, + content = aMessageContent(body = "A regular message"), + ), + ), + ) + factory.timelineItems.test { + factory.replaceWith( + timelineItems = items, + roomMembers = emptyList(), + renderReadReceipts = false, + ) + val bodies = awaitItem() + .filterIsInstance() + .map { (it.content as TimelineItemTextContent).body } + assertThat(bodies).containsExactly("A regular message") + cancelAndIgnoreRemainingEvents() + } + } + private suspend fun TestScope.groupPositionsOf(timestamps: List): List { val factory = aTimelineItemsFactory( config = TimelineItemsFactoryConfig( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogViewTest.kt new file mode 100644 index 0000000000..05388e7227 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogViewTest.kt @@ -0,0 +1,149 @@ +/* + * 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(ExperimentalTestApi::class) + +package io.element.android.features.messages.impl.timeline.sendfailure + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import io.element.android.features.messages.impl.R +import io.element.android.features.messages.impl.timeline.aTimelineItemEvent +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.assertNodeWithTextIsDisplayed +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.ensureCalledOnceWithParam +import io.element.android.tests.testutils.robolectric.RobolectricTest +import io.element.android.tests.testutils.setSafeContent +import org.junit.Test + +class SendFailureDialogViewTest : RobolectricTest() { + @Test + fun `hidden state renders nothing`() = runAndroidComposeUiTest { + setSendFailureDialogView( + sendFailureDialogState = SendFailureDialogState.Hidden, + ) + onNodeWithText(activity!!.getString(CommonStrings.common_sending_failed)).assertDoesNotExist() + } + + @Test + fun `show state renders the error failure reason`() = runAndroidComposeUiTest { + setSendFailureDialogView( + sendFailureDialogState = aSendFailureDialogStateShow( + sendFailureType = SendFailureDialogState.SendFailureType.Error("A failure reason"), + ), + ) + assertNodeWithTextIsDisplayed(CommonStrings.common_sending_failed) + onNodeWithText(activity!!.getString(R.string.error_sending_failure_other, "A failure reason")).assertExists() + } + + @Test + fun `show state renders the invalid mime type failure reason`() = runAndroidComposeUiTest { + setSendFailureDialogView( + sendFailureDialogState = aSendFailureDialogStateShow( + sendFailureType = SendFailureDialogState.SendFailureType.InvalidMimeType("invalid/mimeType"), + ), + ) + onNodeWithText(activity!!.getString(R.string.error_sending_failure_invalid_mime_type, "invalid/mimeType")).assertExists() + } + + @Test + fun `show state renders the missing media content failure reason`() = runAndroidComposeUiTest { + setSendFailureDialogView( + sendFailureDialogState = aSendFailureDialogStateShow( + sendFailureType = SendFailureDialogState.SendFailureType.MissingMediaContent, + ), + ) + onNodeWithText(activity!!.getString(R.string.error_sending_failure_missing_media_content)).assertExists() + } + + @Test + fun `show state renders the sending from unverified device failure reason`() = runAndroidComposeUiTest { + setSendFailureDialogView( + sendFailureDialogState = aSendFailureDialogStateShow( + sendFailureType = SendFailureDialogState.SendFailureType.SendingFromUnverifiedDevice, + ), + ) + onNodeWithText(activity!!.getString(R.string.error_sending_failure_sending_from_unverified_device)).assertExists() + } + + @Test + fun `show state renders the unknown failure reason`() = runAndroidComposeUiTest { + setSendFailureDialogView( + sendFailureDialogState = aSendFailureDialogStateShow( + sendFailureType = SendFailureDialogState.SendFailureType.Unknown, + ), + ) + onNodeWithText(activity!!.getString(R.string.error_sending_failure_unknown)).assertExists() + } + + @Test + fun `clicking on retry invokes the expected callback`() = runAndroidComposeUiTest { + val state = aSendFailureDialogStateShow() + ensureCalledOnceWithParam(state.event) { callback -> + setSendFailureDialogView( + sendFailureDialogState = state, + onRetry = callback, + ) + clickOn(CommonStrings.action_retry) + } + } + + @Test + fun `clicking on remove message invokes the expected callback`() = runAndroidComposeUiTest { + val state = aSendFailureDialogStateShow() + ensureCalledOnceWithParam(state.event) { callback -> + setSendFailureDialogView( + sendFailureDialogState = state, + onRemoveMessage = callback, + ) + clickOn(CommonStrings.action_remove_message) + } + } + + @Test + fun `clicking on cancel invokes the expected callback`() = runAndroidComposeUiTest { + ensureCalledOnce { callback -> + setSendFailureDialogView( + sendFailureDialogState = aSendFailureDialogStateShow(), + onDismiss = callback, + ) + clickOn(CommonStrings.action_cancel) + } + } + + private fun aSendFailureDialogStateShow( + sendFailureType: SendFailureDialogState.SendFailureType = SendFailureDialogState.SendFailureType.Unknown, + ) = SendFailureDialogState.Show( + event = aTimelineItemEvent(sendState = LocalEventSendState.Failed.Unknown("")), + sendFailureType = sendFailureType, + ) + + private fun AndroidComposeUiTest.setSendFailureDialogView( + sendFailureDialogState: SendFailureDialogState, + onDismiss: () -> Unit = EnsureNeverCalled(), + onRetry: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), + onRemoveMessage: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), + ) { + setSafeContent { + SendFailureDialogView( + sendFailureDialogState = sendFailureDialogState, + onDismiss = onDismiss, + onRetry = onRetry, + onRemoveMessage = onRemoveMessage, + ) + } + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt index 7d3b6cf647..fb44bb4e80 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/DefaultVoiceMessageComposerPresenterTest.kt @@ -60,6 +60,7 @@ import org.junit.Rule import org.junit.Test import java.io.File import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @Suppress("LargeClass") @@ -67,8 +68,14 @@ class DefaultVoiceMessageComposerPresenterTest { @get:Rule val warmUpRule = WarmUpRule() + private val startRecordResult = lambdaRecorder { } + private val stopRecordResult = lambdaRecorder { } + private val deleteRecordingResult = lambdaRecorder { } private val voiceRecorder = FakeVoiceRecorder( - recordingDuration = RECORDING_DURATION + recordingDuration = RECORDING_DURATION, + startRecordResult = startRecordResult, + stopRecordResult = stopRecordResult, + deleteRecordingResult = deleteRecordingResult, ) private val analyticsService = FakeAnalyticsService() private val sendVoiceMessageResult = @@ -97,7 +104,9 @@ class DefaultVoiceMessageComposerPresenterTest { companion object { private val RECORDING_DURATION = 1.seconds + private val FIRST_LEVEL_DURATION = 500.milliseconds private val RECORDING_STATE = VoiceMessageState.Recording(RECORDING_DURATION, listOf(0.1f, 0.2f).toImmutableList()) + private val FIRST_RECORDING_STATE = VoiceMessageState.Recording(FIRST_LEVEL_DURATION, listOf(0.1f).toImmutableList()) } @Test @@ -106,7 +115,7 @@ class DefaultVoiceMessageComposerPresenterTest { presenter.test { val initialState = awaitItem() assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) - voiceRecorder.assertCalls(started = 0) + startRecordResult.assertions().isNeverCalled() testPauseAndDestroy(initialState) } @@ -118,9 +127,10 @@ class DefaultVoiceMessageComposerPresenterTest { presenter.test { awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) + assertThat(awaitItem().voiceMessageState).isEqualTo(FIRST_RECORDING_STATE) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) - voiceRecorder.assertCalls(started = 1) + startRecordResult.assertions().isCalledOnce() testPauseAndDestroy(finalState) } @@ -133,13 +143,18 @@ class DefaultVoiceMessageComposerPresenterTest { val voiceRecorder = FakeVoiceRecorder( levels = levels, recordingDuration = RECORDING_DURATION, + startRecordResult = { }, + stopRecordResult = { }, + deleteRecordingResult = { }, ) val presenter = createDefaultVoiceMessageComposerPresenter( voiceRecorder = voiceRecorder, ) presenter.test { awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) - skipItems(numberOfLevels / 2 - 1) + + // Skip until we reach the final state, which should have the last 128 levels + skipItems(numberOfLevels - 1) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isInstanceOf(VoiceMessageState.Recording::class.java) val recordingState = finalState.voiceMessageState as VoiceMessageState.Recording @@ -221,7 +236,8 @@ class DefaultVoiceMessageComposerPresenterTest { val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState()) - voiceRecorder.assertCalls(started = 1, stopped = 1) + startRecordResult.assertions().isCalledOnce() + stopRecordResult.assertions().isCalledOnce().with(value(false)) cancelAndIgnoreRemainingEvents() } @@ -235,7 +251,9 @@ class DefaultVoiceMessageComposerPresenterTest { awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Cancel)) val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) - voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1) + startRecordResult.assertions().isCalledOnce() + stopRecordResult.assertions().isCalledOnce().with(value(true)) + deleteRecordingResult.assertions().isCalledOnce() testPauseAndDestroy(finalState) } } @@ -249,7 +267,9 @@ class DefaultVoiceMessageComposerPresenterTest { val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState()) - voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0) + startRecordResult.assertions().isCalledOnce() + stopRecordResult.assertions().isCalledOnce().with(value(false)) + deleteRecordingResult.assertions().isNeverCalled() testPauseAndDestroy(finalState) } @@ -265,8 +285,10 @@ class DefaultVoiceMessageComposerPresenterTest { } // Nothing should happen - assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) - voiceRecorder.assertCalls(started = 1, stopped = 0, deleted = 0) + assertThat(finalState.voiceMessageState).isEqualTo(FIRST_RECORDING_STATE) + startRecordResult.assertions().isCalledOnce() + stopRecordResult.assertions().isNeverCalled() + deleteRecordingResult.assertions().isNeverCalled() testPauseAndDestroy(finalState) } @@ -282,7 +304,9 @@ class DefaultVoiceMessageComposerPresenterTest { val finalState = awaitItem().also { assertThat(it.voiceMessageState).isEqualTo(aPlayingState()) } - voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0) + startRecordResult.assertions().isCalledOnce() + stopRecordResult.assertions().isCalledOnce().with(value(false)) + deleteRecordingResult.assertions().isNeverCalled() testPauseAndDestroy(finalState) } @@ -299,7 +323,9 @@ class DefaultVoiceMessageComposerPresenterTest { val finalState = awaitItem().also { assertThat(it.voiceMessageState).isEqualTo(aPausedState()) } - voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0) + startRecordResult.assertions().isCalledOnce() + stopRecordResult.assertions().isCalledOnce().with(value(false)) + deleteRecordingResult.assertions().isNeverCalled() testPauseAndDestroy(finalState) } @@ -340,7 +366,9 @@ class DefaultVoiceMessageComposerPresenterTest { val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) - voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1) + startRecordResult.assertions().isCalledOnce() + stopRecordResult.assertions().isCalledOnce().with(value(false)) + deleteRecordingResult.assertions().isCalledOnce() testPauseAndDestroy(finalState) } @@ -360,7 +388,9 @@ class DefaultVoiceMessageComposerPresenterTest { val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) - voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1) + startRecordResult.assertions().isCalledOnce() + stopRecordResult.assertions().isCalledOnce().with(value(false)) + deleteRecordingResult.assertions().isCalledOnce() testPauseAndDestroy(finalState) } @@ -377,7 +407,9 @@ class DefaultVoiceMessageComposerPresenterTest { val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) sendVoiceMessageResult.assertions().isCalledOnce() - voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1) + startRecordResult.assertions().isCalledOnce() + stopRecordResult.assertions().isCalledOnce().with(value(false)) + deleteRecordingResult.assertions().isCalledOnce() testPauseAndDestroy(finalState) } @@ -455,7 +487,9 @@ class DefaultVoiceMessageComposerPresenterTest { val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) sendVoiceMessageResult.assertions().isCalledOnce() - voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1) + startRecordResult.assertions().isCalledOnce() + stopRecordResult.assertions().isCalledOnce().with(value(false)) + deleteRecordingResult.assertions().isCalledOnce() testPauseAndDestroy(finalState) } @@ -476,7 +510,9 @@ class DefaultVoiceMessageComposerPresenterTest { val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) sendVoiceMessageResult.assertions().isCalledOnce() - voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1) + startRecordResult.assertions().isCalledOnce() + stopRecordResult.assertions().isCalledOnce().with(value(false)) + deleteRecordingResult.assertions().isCalledOnce() testPauseAndDestroy(finalState) } @@ -499,7 +535,9 @@ class DefaultVoiceMessageComposerPresenterTest { assertThat(finalState.voiceMessageState).isEqualTo(aPreviewState(isSending = true)) sendVoiceMessageResult.assertions().isNeverCalled() assertThat(analyticsService.trackedErrors).isEmpty() - voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 0) + startRecordResult.assertions().isCalledOnce() + stopRecordResult.assertions().isCalledOnce().with(value(false)) + deleteRecordingResult.assertions().isNeverCalled() testPauseAndDestroy(finalState) } @@ -527,7 +565,9 @@ class DefaultVoiceMessageComposerPresenterTest { val finalState = awaitItem() assertThat(finalState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) sendVoiceMessageResult.assertions().isCalledOnce() - voiceRecorder.assertCalls(started = 1, stopped = 1, deleted = 1) + startRecordResult.assertions().isCalledOnce() + stopRecordResult.assertions().isCalledOnce().with(value(false)) + deleteRecordingResult.assertions().isCalledOnce() testPauseAndDestroy(finalState) } @@ -577,7 +617,7 @@ class DefaultVoiceMessageComposerPresenterTest { assertThat(initialState.voiceMessageState).isEqualTo(VoiceMessageState.Idle) sendVoiceMessageResult.assertions().isNeverCalled() assertThat(analyticsService.trackedErrors).hasSize(1) - voiceRecorder.assertCalls(started = 0) + startRecordResult.assertions().isNeverCalled() testPauseAndDestroy(initialState) } @@ -586,8 +626,14 @@ class DefaultVoiceMessageComposerPresenterTest { @Test fun `present - record error - security exceptions are tracked`() = runTest { val exception = SecurityException("") - voiceRecorder.givenThrowsSecurityException(exception) - val presenter = createDefaultVoiceMessageComposerPresenter() + val startRecordResult = lambdaRecorder { throw exception } + val voiceRecorder = FakeVoiceRecorder( + recordingDuration = RECORDING_DURATION, + startRecordResult = startRecordResult, + ) + val presenter = createDefaultVoiceMessageComposerPresenter( + voiceRecorder = voiceRecorder, + ) presenter.test { val initialState = awaitItem() initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) @@ -596,7 +642,7 @@ class DefaultVoiceMessageComposerPresenterTest { assertThat(analyticsService.trackedErrors).containsExactly( VoiceMessageException.PermissionMissing(message = "Expected permission to record but none", cause = exception) ) - voiceRecorder.assertCalls(started = 1) + startRecordResult.assertions().isCalledOnce() testPauseAndDestroy(initialState) } @@ -616,7 +662,8 @@ class DefaultVoiceMessageComposerPresenterTest { assertThat(awaitItem().voiceMessageState).isEqualTo(VoiceMessageState.Idle) initialState.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Stop)) - voiceRecorder.assertCalls(stopped = 1) + startRecordResult.assertions().isNeverCalled() + stopRecordResult.assertions().isCalledOnce().with(value(false)) permissionsPresenter.setPermissionGranted() @@ -625,7 +672,8 @@ class DefaultVoiceMessageComposerPresenterTest { val finalState = expectMostRecentItem() assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) - voiceRecorder.assertCalls(stopped = 1, started = 1) + startRecordResult.assertions().isCalledOnce() + stopRecordResult.assertions().isCalledOnce().with(value(false)) testPauseAndDestroy(finalState) } @@ -659,7 +707,7 @@ class DefaultVoiceMessageComposerPresenterTest { val finalState = expectMostRecentItem() assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) - voiceRecorder.assertCalls(started = 1) + startRecordResult.assertions().isCalledOnce() testPauseAndDestroy(finalState) } @@ -696,7 +744,7 @@ class DefaultVoiceMessageComposerPresenterTest { assertThat(it.voiceMessageState).isEqualTo(VoiceMessageState.Idle) assertThat(it.showPermissionRationaleDialog).isTrue() } - voiceRecorder.assertCalls(started = 0) + startRecordResult.assertions().isNeverCalled() cancelAndIgnoreRemainingEvents() testPauseAndDestroy(finalState) diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationStateProvider.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationStatePreviewParam.kt similarity index 90% rename from features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationStateProvider.kt rename to features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationStatePreviewParam.kt index 8f738d3207..de25284867 100644 --- a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationStateProvider.kt +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationStatePreviewParam.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.api.MigrationState import io.element.android.libraries.architecture.AsyncData -internal class MigrationStateProvider : PreviewParameterProvider { +internal class MigrationStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMigrationState(), diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationView.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationView.kt index a294c32534..c7f4dd678a 100644 --- a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationView.kt +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/MigrationView.kt @@ -49,7 +49,7 @@ fun MigrationView( @PreviewsDayNight @Composable internal fun MigrationViewPreview( - @PreviewParameter(MigrationStateProvider::class) state: MigrationState, + @PreviewParameter(MigrationStatePreviewParam::class) state: MigrationState, ) = ElementPreview { MigrationView( migrationState = state, diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStatePreviewParam.kt similarity index 98% rename from features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt rename to features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStatePreviewParam.kt index 20d9f0d557..9c7a505421 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStatePreviewParam.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.matrix.api.poll.PollKind import kotlinx.collections.immutable.toImmutableList -class CreatePollStateProvider : PreviewParameterProvider { +class CreatePollStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aCreatePollState( diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt index 6163fe19ad..a20c1f5af1 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt @@ -232,7 +232,7 @@ private fun CreatePollTopAppBar( @PreviewsDayNight @Composable internal fun CreatePollViewPreview( - @PreviewParameter(CreatePollStateProvider::class) state: CreatePollState + @PreviewParameter(CreatePollStatePreviewParam::class) state: CreatePollState ) = ElementPreview { CreatePollView( state = state, diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryEvents.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryEvent.kt similarity index 81% rename from features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryEvents.kt rename to features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryEvent.kt index 14ce13d67a..9d0d699d89 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryEvents.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryEvent.kt @@ -11,9 +11,9 @@ package io.element.android.features.poll.impl.history import io.element.android.features.poll.impl.history.model.PollHistoryFilter import io.element.android.libraries.matrix.api.core.EventId -sealed interface PollHistoryEvents { - data object LoadMore : PollHistoryEvents - data class SelectPollAnswer(val pollStartId: EventId, val answerId: String) : PollHistoryEvents - data class EndPoll(val pollStartId: EventId) : PollHistoryEvents - data class SelectFilter(val filter: PollHistoryFilter) : PollHistoryEvents +sealed interface PollHistoryEvent { + data object LoadMore : PollHistoryEvent + data class SelectPollAnswer(val pollStartId: EventId, val answerId: String) : PollHistoryEvent + data class EndPoll(val pollStartId: EventId) : PollHistoryEvent + data class SelectFilter(val filter: PollHistoryFilter) : PollHistoryEvent } diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt index 434e2ed18c..74a511da45 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenter.kt @@ -63,22 +63,22 @@ class PollHistoryPresenter( } } val coroutineScope = rememberCoroutineScope() - fun handleEvent(event: PollHistoryEvents) { + fun handleEvent(event: PollHistoryEvent) { when (event) { - is PollHistoryEvents.LoadMore -> { + is PollHistoryEvent.LoadMore -> { coroutineScope.loadMore(timeline) } - is PollHistoryEvents.SelectPollAnswer -> sessionCoroutineScope.launch { + is PollHistoryEvent.SelectPollAnswer -> sessionCoroutineScope.launch { sendPollResponseAction.execute( timeline = timeline, pollStartId = event.pollStartId, answerId = event.answerId ) } - is PollHistoryEvents.EndPoll -> sessionCoroutineScope.launch { + is PollHistoryEvent.EndPoll -> sessionCoroutineScope.launch { endPollAction.execute(timeline = timeline, pollStartId = event.pollStartId) } - is PollHistoryEvents.SelectFilter -> { + is PollHistoryEvent.SelectFilter -> { activeFilter = event.filter } } diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryState.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryState.kt index bddfd153f6..67225e85c2 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryState.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryState.kt @@ -18,7 +18,7 @@ data class PollHistoryState( val hasMoreToLoad: Boolean, val activeFilter: PollHistoryFilter, val pollHistoryItems: PollHistoryItems, - val eventSink: (PollHistoryEvents) -> Unit, + val eventSink: (PollHistoryEvent) -> Unit, ) { fun pollHistoryForFilter(filter: PollHistoryFilter): ImmutableList { return when (filter) { diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryStateProvider.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryStatePreviewParam.kt similarity index 94% rename from features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryStateProvider.kt rename to features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryStatePreviewParam.kt index a08550f2a1..40144f261e 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryStateProvider.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryStatePreviewParam.kt @@ -16,7 +16,7 @@ import io.element.android.features.poll.impl.history.model.PollHistoryItem import io.element.android.features.poll.impl.history.model.PollHistoryItems import kotlinx.collections.immutable.toImmutableList -class PollHistoryStateProvider : PreviewParameterProvider { +class PollHistoryStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aPollHistoryState(), @@ -48,7 +48,7 @@ internal fun aPollHistoryState( currentItems: List = listOf( aPollHistoryItem(), ), - eventSink: (PollHistoryEvents) -> Unit = {}, + eventSink: (PollHistoryEvent) -> Unit = {}, ) = PollHistoryState( isLoading = isLoading, hasMoreToLoad = hasMoreToLoad, diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryView.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryView.kt index 9a654c2abb..61c7809f49 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryView.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/PollHistoryView.kt @@ -62,15 +62,15 @@ fun PollHistoryView( modifier: Modifier = Modifier, ) { fun onLoadMore() { - state.eventSink(PollHistoryEvents.LoadMore) + state.eventSink(PollHistoryEvent.LoadMore) } fun onSelectAnswer(pollStartId: EventId, answerId: String) { - state.eventSink(PollHistoryEvents.SelectPollAnswer(pollStartId, answerId)) + state.eventSink(PollHistoryEvent.SelectPollAnswer(pollStartId, answerId)) } fun onEndPoll(pollStartId: EventId) { - state.eventSink(PollHistoryEvents.EndPoll(pollStartId)) + state.eventSink(PollHistoryEvent.EndPoll(pollStartId)) } Scaffold( @@ -97,7 +97,7 @@ fun PollHistoryView( } PollHistoryFilterButtons( activeFilter = state.activeFilter, - onSelectFilter = { state.eventSink(PollHistoryEvents.SelectFilter(it)) }, + onSelectFilter = { state.eventSink(PollHistoryEvent.SelectFilter(it)) }, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 8.dp), @@ -255,7 +255,7 @@ private fun PollHistoryItemRow( @PreviewsDayNight @Composable internal fun PollHistoryViewPreview( - @PreviewParameter(PollHistoryStateProvider::class) state: PollHistoryState + @PreviewParameter(PollHistoryStatePreviewParam::class) state: PollHistoryState ) = ElementPreview { PollHistoryView( state = state, diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt index 8d4cedc6a8..1342f89c60 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt @@ -88,12 +88,12 @@ class PollHistoryPresenterTest { }.test { awaitItem().also { state -> assertThat(state.activeFilter).isEqualTo(PollHistoryFilter.ONGOING) - state.eventSink(PollHistoryEvents.SelectFilter(PollHistoryFilter.PAST)) + state.eventSink(PollHistoryEvent.SelectFilter(PollHistoryFilter.PAST)) } skipItems(1) awaitItem().also { state -> assertThat(state.activeFilter).isEqualTo(PollHistoryFilter.PAST) - state.eventSink(PollHistoryEvents.SelectFilter(PollHistoryFilter.ONGOING)) + state.eventSink(PollHistoryEvent.SelectFilter(PollHistoryFilter.ONGOING)) } awaitItem().also { state -> assertThat(state.activeFilter).isEqualTo(PollHistoryFilter.ONGOING) @@ -114,10 +114,10 @@ class PollHistoryPresenterTest { presenter.present() }.test { val state = awaitItem() - state.eventSink(PollHistoryEvents.EndPoll(AN_EVENT_ID)) + state.eventSink(PollHistoryEvent.EndPoll(AN_EVENT_ID)) runCurrent() endPollAction.verifyExecutionCount(1) - state.eventSink(PollHistoryEvents.SelectPollAnswer(AN_EVENT_ID, "answer")) + state.eventSink(PollHistoryEvent.SelectPollAnswer(AN_EVENT_ID, "answer")) runCurrent() sendPollResponseAction.verifyExecutionCount(1) cancelAndConsumeRemainingEvents() @@ -139,7 +139,7 @@ class PollHistoryPresenterTest { skipItems(1) val loadedState = awaitItem() assertThat(loadedState.isLoading).isFalse() - loadedState.eventSink(PollHistoryEvents.LoadMore) + loadedState.eventSink(PollHistoryEvent.LoadMore) backwardPaginationStatus.getAndUpdate { it.copy(isPaginating = true) } awaitItem().also { state -> assertThat(state.isLoading).isTrue() diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryViewTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryViewTest.kt index ee6ad69ba6..1b74118734 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryViewTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryViewTest.kt @@ -35,7 +35,7 @@ import org.robolectric.annotation.Config class PollHistoryViewTest : RobolectricTest() { @Test fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { setPollHistoryViewView( aPollHistoryState( @@ -50,7 +50,7 @@ class PollHistoryViewTest : RobolectricTest() { @Config(qualifiers = "h1024dp") @Test fun `clicking on edit poll invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) val eventId = EventId("\$anEventId") val state = aPollHistoryState( currentItems = listOf( @@ -76,7 +76,7 @@ class PollHistoryViewTest : RobolectricTest() { @Config(qualifiers = "h1024dp") @Test fun `clicking on poll end emits the expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() val eventId = EventId("\$anEventId") val state = aPollHistoryState( currentItems = listOf( @@ -102,14 +102,14 @@ class PollHistoryViewTest : RobolectricTest() { eventsRecorder.assertEmpty() clickOn(CommonStrings.action_ok) eventsRecorder.assertSingle( - PollHistoryEvents.EndPoll(eventId) + PollHistoryEvent.EndPoll(eventId) ) } @Config(qualifiers = "h1024dp") @Test fun `clicking on poll answer emits the expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() val eventId = EventId("\$anEventId") val state = aPollHistoryState( currentItems = listOf( @@ -133,13 +133,13 @@ class PollHistoryViewTest : RobolectricTest() { useUnmergedTree = true, ).performClick() eventsRecorder.assertSingle( - PollHistoryEvents.SelectPollAnswer(eventId, answer.id) + PollHistoryEvent.SelectPollAnswer(eventId, answer.id) ) } @Test fun `clicking on past tab emits the expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setPollHistoryViewView( aPollHistoryState( eventSink = eventsRecorder @@ -147,14 +147,14 @@ class PollHistoryViewTest : RobolectricTest() { ) clickOn(R.string.screen_polls_history_filter_past) eventsRecorder.assertSingle( - PollHistoryEvents.SelectFilter(filter = PollHistoryFilter.PAST) + PollHistoryEvent.SelectFilter(filter = PollHistoryFilter.PAST) ) } @Config(qualifiers = "h1024dp") @Test fun `clicking on load more emits the expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setPollHistoryViewView( aPollHistoryState( hasMoreToLoad = true, @@ -163,7 +163,7 @@ class PollHistoryViewTest : RobolectricTest() { ) clickOn(CommonStrings.action_load_more) eventsRecorder.assertSingle( - PollHistoryEvents.LoadMore + PollHistoryEvent.LoadMore ) } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutStatePreviewParam.kt similarity index 90% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutStateProvider.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutStatePreviewParam.kt index 561fe53a49..a15a457302 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutStatePreviewParam.kt @@ -11,7 +11,7 @@ package io.element.android.features.preferences.impl.about import androidx.compose.ui.tooling.preview.PreviewParameterProvider import kotlinx.collections.immutable.toImmutableList -open class AboutStateProvider : PreviewParameterProvider { +open class AboutStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( anAboutState(), diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt index 3a6671f692..79a17b3e5a 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/about/AboutView.kt @@ -51,7 +51,7 @@ fun AboutView( @PreviewsDayNight @Composable -internal fun AboutViewPreview(@PreviewParameter(AboutStateProvider::class) state: AboutState) = ElementPreview { +internal fun AboutViewPreview(@PreviewParameter(AboutStatePreviewParam::class) state: AboutState) = ElementPreview { AboutView( state = state, onElementLegalClick = {}, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvent.kt similarity index 80% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvent.kt index ef27c499af..f2e6537d9d 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvent.kt @@ -11,14 +11,14 @@ package io.element.android.features.preferences.impl.advanced import io.element.android.libraries.matrix.api.media.MediaPreviewValue import io.element.android.libraries.preferences.api.store.VideoCompressionPreset -sealed interface AdvancedSettingsEvents { - data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents - data class SetSharePresenceEnabled(val enabled: Boolean) : AdvancedSettingsEvents - data class SetCompressMedia(val compress: Boolean) : AdvancedSettingsEvents - data class SetCompressImages(val compress: Boolean) : AdvancedSettingsEvents - data class SetVideoUploadQuality(val videoPreset: VideoCompressionPreset) : AdvancedSettingsEvents - data class SetTheme(val theme: ThemeOption) : AdvancedSettingsEvents - data class SetTimelineMediaPreviewValue(val value: MediaPreviewValue) : AdvancedSettingsEvents - data class SetHideInviteAvatars(val value: Boolean) : AdvancedSettingsEvents - data class SetLiveLocationMinimumDistanceUpdate(val value: Int) : AdvancedSettingsEvents +sealed interface AdvancedSettingsEvent { + data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvent + data class SetSharePresenceEnabled(val enabled: Boolean) : AdvancedSettingsEvent + data class SetCompressMedia(val compress: Boolean) : AdvancedSettingsEvent + data class SetCompressImages(val compress: Boolean) : AdvancedSettingsEvent + data class SetVideoUploadQuality(val videoPreset: VideoCompressionPreset) : AdvancedSettingsEvent + data class SetTheme(val theme: ThemeOption) : AdvancedSettingsEvent + data class SetTimelineMediaPreviewValue(val value: MediaPreviewValue) : AdvancedSettingsEvent + data class SetHideInviteAvatars(val value: Boolean) : AdvancedSettingsEvent + data class SetLiveLocationMinimumDistanceUpdate(val value: Int) : AdvancedSettingsEvent } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt index 45c136ab62..3ebf1f5140 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt @@ -100,18 +100,18 @@ class AdvancedSettingsPresenter( }.collect() } - fun handleEvent(event: AdvancedSettingsEvents) { + fun handleEvent(event: AdvancedSettingsEvent) { when (event) { - is AdvancedSettingsEvents.SetDeveloperModeEnabled -> sessionCoroutineScope.launch { + is AdvancedSettingsEvent.SetDeveloperModeEnabled -> sessionCoroutineScope.launch { appPreferencesStore.setDeveloperModeEnabled(event.enabled) } - is AdvancedSettingsEvents.SetSharePresenceEnabled -> sessionCoroutineScope.launch { + is AdvancedSettingsEvent.SetSharePresenceEnabled -> sessionCoroutineScope.launch { sessionPreferencesStore.setSharePresence(event.enabled) } - is AdvancedSettingsEvents.SetCompressMedia -> sessionCoroutineScope.launch { + is AdvancedSettingsEvent.SetCompressMedia -> sessionCoroutineScope.launch { sessionPreferencesStore.setOptimizeImages(event.compress) } - is AdvancedSettingsEvents.SetTheme -> sessionCoroutineScope.launch { + is AdvancedSettingsEvent.SetTheme -> sessionCoroutineScope.launch { when (event.theme) { ThemeOption.System -> appPreferencesStore.setTheme(Theme.System.name) ThemeOption.Dark -> appPreferencesStore.setTheme(Theme.Dark.name) @@ -119,15 +119,15 @@ class AdvancedSettingsPresenter( ThemeOption.Light -> appPreferencesStore.setTheme(Theme.Light.name) } } - is AdvancedSettingsEvents.SetHideInviteAvatars -> mediaPreviewConfigStateStore.setHideInviteAvatars(event.value) - is AdvancedSettingsEvents.SetTimelineMediaPreviewValue -> mediaPreviewConfigStateStore.setTimelineMediaPreviewValue(event.value) - is AdvancedSettingsEvents.SetLiveLocationMinimumDistanceUpdate -> sessionCoroutineScope.launch { + is AdvancedSettingsEvent.SetHideInviteAvatars -> mediaPreviewConfigStateStore.setHideInviteAvatars(event.value) + is AdvancedSettingsEvent.SetTimelineMediaPreviewValue -> mediaPreviewConfigStateStore.setTimelineMediaPreviewValue(event.value) + is AdvancedSettingsEvent.SetLiveLocationMinimumDistanceUpdate -> sessionCoroutineScope.launch { appPreferencesStore.setLiveLocationMinimumDistanceInMetersUpdate(event.value) } - is AdvancedSettingsEvents.SetCompressImages -> sessionCoroutineScope.launch { + is AdvancedSettingsEvent.SetCompressImages -> sessionCoroutineScope.launch { sessionPreferencesStore.setOptimizeImages(event.compress) } - is AdvancedSettingsEvents.SetVideoUploadQuality -> sessionCoroutineScope.launch { + is AdvancedSettingsEvent.SetVideoUploadQuality -> sessionCoroutineScope.launch { sessionPreferencesStore.setVideoCompressionPreset(event.videoPreset) } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt index 94019c1f01..689075759d 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt @@ -24,7 +24,7 @@ data class AdvancedSettingsState( val availableThemeOptions: ImmutableList, val mediaPreviewConfigState: MediaPreviewConfigState, val liveLocationMinimumDistanceUpdate: Int?, - val eventSink: (AdvancedSettingsEvents) -> Unit + val eventSink: (AdvancedSettingsEvent) -> Unit ) sealed interface MediaOptimizationState { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStatePreviewParam.kt similarity index 95% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStatePreviewParam.kt index 2df59e3ced..3b6a323419 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStatePreviewParam.kt @@ -15,7 +15,7 @@ import io.element.android.libraries.preferences.api.store.VideoCompressionPreset import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -open class AdvancedSettingsStateProvider : PreviewParameterProvider { +open class AdvancedSettingsStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aAdvancedSettingsState(), @@ -44,7 +44,7 @@ fun aAdvancedSettingsState( liveLocationMinimumDistanceUpdate: Int? = 50, setTimelineMediaPreviewAction: AsyncAction = AsyncAction.Uninitialized, setHideInviteAvatarsAction: AsyncAction = AsyncAction.Uninitialized, - eventSink: (AdvancedSettingsEvents) -> Unit = {}, + eventSink: (AdvancedSettingsEvent) -> Unit = {}, ) = AdvancedSettingsState( isDeveloperModeEnabled = isDeveloperModeEnabled, isSharePresenceEnabled = isSharePresenceEnabled, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt index 438b20049a..d7b1cfeb17 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt @@ -89,7 +89,7 @@ fun AdvancedSettingsView( selectedOption = state.theme, options = state.availableThemeOptions, onSelectOption = { themeOption -> - state.eventSink(AdvancedSettingsEvents.SetTheme(themeOption)) + state.eventSink(AdvancedSettingsEvent.SetTheme(themeOption)) } ) ListItem( @@ -102,7 +102,7 @@ fun AdvancedSettingsView( trailingContent = ListItemContent.Switch( checked = state.isDeveloperModeEnabled, ), - onClick = { state.eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(!state.isDeveloperModeEnabled)) } + onClick = { state.eventSink(AdvancedSettingsEvent.SetDeveloperModeEnabled(!state.isDeveloperModeEnabled)) } ) ListItem( content = { @@ -114,7 +114,7 @@ fun AdvancedSettingsView( trailingContent = ListItemContent.Switch( checked = state.isSharePresenceEnabled, ), - onClick = { state.eventSink(AdvancedSettingsEvents.SetSharePresenceEnabled(!state.isSharePresenceEnabled)) } + onClick = { state.eventSink(AdvancedSettingsEvent.SetSharePresenceEnabled(!state.isSharePresenceEnabled)) } ) val compressImages = state.mediaOptimizationState?.shouldCompressImages @@ -140,7 +140,7 @@ fun AdvancedSettingsView( Interaction.Name.MobileSettingsOptimizeMediaUploadsDisabled } ) - state.eventSink(AdvancedSettingsEvents.SetCompressMedia(newValue)) + state.eventSink(AdvancedSettingsEvent.SetCompressMedia(newValue)) } ) } @@ -164,7 +164,7 @@ fun AdvancedSettingsView( Interaction.Name.MobileSettingsOptimizeMediaUploadsDisabled } ) - state.eventSink(AdvancedSettingsEvents.SetCompressMedia(newValue)) + state.eventSink(AdvancedSettingsEvent.SetCompressMedia(newValue)) } ) @@ -193,7 +193,7 @@ fun AdvancedSettingsView( VideoQualitySelectorDialog( selectedPreset = state.mediaOptimizationState.videoPreset, onSubmit = { preset -> - state.eventSink(AdvancedSettingsEvents.SetVideoUploadQuality(preset)) + state.eventSink(AdvancedSettingsEvent.SetVideoUploadQuality(preset)) displaySelectorDialog = false }, onDismiss = { displaySelectorDialog = false }, @@ -207,7 +207,7 @@ fun AdvancedSettingsView( LiveLocationUpdatesSection( value = state.liveLocationMinimumDistanceUpdate, onSaveValue = { value -> - state.eventSink(AdvancedSettingsEvents.SetLiveLocationMinimumDistanceUpdate(value)) + state.eventSink(AdvancedSettingsEvent.SetLiveLocationMinimumDistanceUpdate(value)) }, onOpenAppPermissionsClick = onOpenAppSettingsClick, ) @@ -286,7 +286,7 @@ private fun ModerationAndSafety( title = stringResource(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title), isChecked = state.mediaPreviewConfigState.hideInviteAvatars, onCheckedChange = { - state.eventSink(AdvancedSettingsEvents.SetHideInviteAvatars(it)) + state.eventSink(AdvancedSettingsEvent.SetHideInviteAvatars(it)) }, enabled = !state.mediaPreviewConfigState.setHideInviteAvatarsAction.isLoading() ) @@ -307,7 +307,7 @@ private fun ModerationAndSafety( compact = true ), onClick = { - state.eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Off)) + state.eventSink(AdvancedSettingsEvent.SetTimelineMediaPreviewValue(MediaPreviewValue.Off)) }, enabled = !state.mediaPreviewConfigState.setTimelineMediaPreviewAction.isLoading() ) @@ -318,7 +318,7 @@ private fun ModerationAndSafety( compact = true ), onClick = { - state.eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Private)) + state.eventSink(AdvancedSettingsEvent.SetTimelineMediaPreviewValue(MediaPreviewValue.Private)) }, enabled = !state.mediaPreviewConfigState.setTimelineMediaPreviewAction.isLoading() ) @@ -329,7 +329,7 @@ private fun ModerationAndSafety( compact = true ), onClick = { - state.eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.On)) + state.eventSink(AdvancedSettingsEvent.SetTimelineMediaPreviewValue(MediaPreviewValue.On)) }, enabled = !state.mediaPreviewConfigState.setTimelineMediaPreviewAction.isLoading() ) @@ -410,17 +410,17 @@ private fun LiveLocationUpdatesSection( @PreviewWithLargeHeight @Composable -internal fun AdvancedSettingsViewLightPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) = +internal fun AdvancedSettingsViewLightPreview(@PreviewParameter(AdvancedSettingsStatePreviewParam::class) state: AdvancedSettingsState) = ElementPreviewLight { ContentToPreview(state) } @PreviewWithLargeHeight @Composable -internal fun AdvancedSettingsViewDarkPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) = +internal fun AdvancedSettingsViewDarkPreview(@PreviewParameter(AdvancedSettingsStatePreviewParam::class) state: AdvancedSettingsState) = ElementPreviewDark { ContentToPreview(state) } @PreviewWithLargeHeight @Composable -internal fun AdvancedSettingsViewBlackPreview(@PreviewParameter(AdvancedSettingsStateProvider::class) state: AdvancedSettingsState) = +internal fun AdvancedSettingsViewBlackPreview(@PreviewParameter(AdvancedSettingsStatePreviewParam::class) state: AdvancedSettingsState) = ElementPreviewBlack { ContentToPreview(state) } @ExcludeFromCoverage diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsStatePreviewParam.kt similarity index 87% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsStateProvider.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsStatePreviewParam.kt index 9e15ea2f35..ab7a8aae3d 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsStatePreviewParam.kt @@ -11,7 +11,7 @@ package io.element.android.features.preferences.impl.analytics import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.analytics.api.preferences.aAnalyticsPreferencesState -open class AnalyticsSettingsStateProvider : PreviewParameterProvider { +open class AnalyticsSettingsStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aAnalyticsSettingsState(), diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt index ef84600e2e..61fc3216f2 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt @@ -37,7 +37,7 @@ fun AnalyticsSettingsView( @PreviewsDayNight @Composable -internal fun AnalyticsSettingsViewPreview(@PreviewParameter(AnalyticsSettingsStateProvider::class) state: AnalyticsSettingsState) = ElementPreview { +internal fun AnalyticsSettingsViewPreview(@PreviewParameter(AnalyticsSettingsStatePreviewParam::class) state: AnalyticsSettingsState) = ElementPreview { AnalyticsSettingsView( state = state, onBackClick = {}, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersEvent.kt similarity index 67% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersEvents.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersEvent.kt index 31e516e730..42fdff63eb 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersEvent.kt @@ -10,9 +10,9 @@ package io.element.android.features.preferences.impl.blockedusers import io.element.android.libraries.matrix.api.core.UserId -sealed interface BlockedUsersEvents { - data class Unblock(val userId: UserId) : BlockedUsersEvents - data class CopyToClipboard(val userId: UserId) : BlockedUsersEvents - data object ConfirmUnblock : BlockedUsersEvents - data object Cancel : BlockedUsersEvents +sealed interface BlockedUsersEvent { + data class Unblock(val userId: UserId) : BlockedUsersEvent + data class CopyToClipboard(val userId: UserId) : BlockedUsersEvent + data object ConfirmUnblock : BlockedUsersEvent + data object Cancel : BlockedUsersEvent } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt index 2944089fa1..8f20694d81 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenter.kt @@ -74,23 +74,23 @@ class BlockedUsersPresenter( val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() - fun handleEvent(event: BlockedUsersEvents) { + fun handleEvent(event: BlockedUsersEvent) { when (event) { - is BlockedUsersEvents.Unblock -> { + is BlockedUsersEvent.Unblock -> { pendingUserToUnblock = event.userId unblockUserAction.value = AsyncAction.ConfirmingNoParams } - is BlockedUsersEvents.CopyToClipboard -> { + is BlockedUsersEvent.CopyToClipboard -> { clipboardHelper.copyPlainText(event.userId.value) snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard)) } - BlockedUsersEvents.ConfirmUnblock -> { + BlockedUsersEvent.ConfirmUnblock -> { pendingUserToUnblock?.let { coroutineScope.unblockUser(it, unblockUserAction) pendingUserToUnblock = null } } - BlockedUsersEvents.Cancel -> { + BlockedUsersEvent.Cancel -> { pendingUserToUnblock = null unblockUserAction.value = AsyncAction.Uninitialized } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersState.kt index e4b07386c7..d62ab03bff 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersState.kt @@ -17,5 +17,5 @@ data class BlockedUsersState( val blockedUsers: ImmutableList, val unblockUserAction: AsyncAction, val snackbarMessage: SnackbarMessage?, - val eventSink: (BlockedUsersEvents) -> Unit, + val eventSink: (BlockedUsersEvent) -> Unit, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStatePreviewParam.kt similarity index 93% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStatePreviewParam.kt index 0e0e9e5ca5..6fce0ba1f1 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersStatePreviewParam.kt @@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUserList import kotlinx.collections.immutable.toImmutableList -class BlockedUsersStateProvider : PreviewParameterProvider { +class BlockedUsersStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aBlockedUsersState(), @@ -32,7 +32,7 @@ internal fun aBlockedUsersState( blockedUsers: List = aMatrixUserList(), unblockUserAction: AsyncAction = AsyncAction.Uninitialized, snackbarMessage: SnackbarMessage? = null, - eventSink: (BlockedUsersEvents) -> Unit = {}, + eventSink: (BlockedUsersEvent) -> Unit = {}, ): BlockedUsersState { return BlockedUsersState( blockedUsers = blockedUsers.toImmutableList(), diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersView.kt index c4d8387295..be6c0ef170 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersView.kt @@ -64,8 +64,8 @@ fun BlockedUsersView( items(state.blockedUsers) { matrixUser -> BlockedUserItem( matrixUser = matrixUser, - onClick = { state.eventSink(BlockedUsersEvents.Unblock(it)) }, - onLongClick = { state.eventSink(BlockedUsersEvents.CopyToClipboard(it)) }, + onClick = { state.eventSink(BlockedUsersEvent.Unblock(it)) }, + onLongClick = { state.eventSink(BlockedUsersEvent.CopyToClipboard(it)) }, ) } } @@ -99,8 +99,8 @@ fun BlockedUsersView( title = stringResource(R.string.screen_blocked_users_unblock_alert_title), content = stringResource(R.string.screen_blocked_users_unblock_alert_description), submitText = stringResource(R.string.screen_blocked_users_unblock_alert_action), - onSubmitClick = { state.eventSink(BlockedUsersEvents.ConfirmUnblock) }, - onDismiss = { state.eventSink(BlockedUsersEvents.Cancel) } + onSubmitClick = { state.eventSink(BlockedUsersEvent.ConfirmUnblock) }, + onDismiss = { state.eventSink(BlockedUsersEvent.Cancel) } ) } else -> Unit @@ -126,7 +126,7 @@ private fun BlockedUserItem( @PreviewsDayNight @Composable -internal fun BlockedUsersViewPreview(@PreviewParameter(BlockedUsersStateProvider::class) state: BlockedUsersState) { +internal fun BlockedUsersViewPreview(@PreviewParameter(BlockedUsersStatePreviewParam::class) state: BlockedUsersState) { ElementPreview { BlockedUsersView( state = state, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvent.kt similarity index 73% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvent.kt index 2d6c2b0904..e0ad208fc1 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvent.kt @@ -10,11 +10,11 @@ package io.element.android.features.preferences.impl.developer import androidx.compose.ui.graphics.Color -sealed interface DeveloperSettingsEvents { - data class SetShowColorPicker(val show: Boolean) : DeveloperSettingsEvents - data class ChangeBrandColor(val color: Color?) : DeveloperSettingsEvents - data object ClearCache : DeveloperSettingsEvents - data object VacuumStores : DeveloperSettingsEvents - data class MarkAllRoomsAsRead(val needsConfirmation: Boolean) : DeveloperSettingsEvents - data object DismissMarkAllRoomsAsReadConfirmation : DeveloperSettingsEvents +sealed interface DeveloperSettingsEvent { + data class SetShowColorPicker(val show: Boolean) : DeveloperSettingsEvent + data class ChangeBrandColor(val color: Color?) : DeveloperSettingsEvent + data object ClearCache : DeveloperSettingsEvent + data object VacuumStores : DeveloperSettingsEvent + data class MarkAllRoomsAsRead(val needsConfirmation: Boolean) : DeveloperSettingsEvent + data object DismissMarkAllRoomsAsReadConfirmation : DeveloperSettingsEvent } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index c5a9b430ee..c925433608 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -79,10 +79,10 @@ class DeveloperSettingsPresenter( computeCacheSize(cacheSize) } - fun handleEvent(event: DeveloperSettingsEvents) { + fun handleEvent(event: DeveloperSettingsEvent) { when (event) { - DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction) - is DeveloperSettingsEvents.ChangeBrandColor -> coroutineScope.launch { + DeveloperSettingsEvent.ClearCache -> coroutineScope.clearCache(clearCacheAction) + is DeveloperSettingsEvent.ChangeBrandColor -> coroutineScope.launch { showColorPicker = false val color = event.color ?.toArgb() @@ -91,13 +91,13 @@ class DeveloperSettingsPresenter( ?.padStart(7, '#') enterpriseService.overrideBrandColor(sessionId, color) } - is DeveloperSettingsEvents.SetShowColorPicker -> { + is DeveloperSettingsEvent.SetShowColorPicker -> { showColorPicker = event.show } - DeveloperSettingsEvents.VacuumStores -> coroutineScope.launch { + DeveloperSettingsEvent.VacuumStores -> coroutineScope.launch { vacuumStoresUseCase() } - is DeveloperSettingsEvents.MarkAllRoomsAsRead -> { + is DeveloperSettingsEvent.MarkAllRoomsAsRead -> { if (event.needsConfirmation) { markAllRoomsAsReadAction.value = AsyncAction.ConfirmingNoParams } else { @@ -106,7 +106,7 @@ class DeveloperSettingsPresenter( ) } } - DeveloperSettingsEvents.DismissMarkAllRoomsAsReadConfirmation -> { + DeveloperSettingsEvent.DismissMarkAllRoomsAsReadConfirmation -> { markAllRoomsAsReadAction.value = AsyncAction.Uninitialized } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt index 700c989bfa..3a859f14bb 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt @@ -23,7 +23,7 @@ data class DeveloperSettingsState( val isEnterpriseBuild: Boolean, val showColorPicker: Boolean, val deviceId: DeviceId, - val eventSink: (DeveloperSettingsEvents) -> Unit + val eventSink: (DeveloperSettingsEvent) -> Unit ) { val showLoader = clearCacheAction is AsyncAction.Loading || markAllRoomsAsReadAction is AsyncAction.Loading } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStatePreviewParam.kt similarity index 94% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStatePreviewParam.kt index b14446d11c..cbe95d6b20 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStatePreviewParam.kt @@ -16,7 +16,7 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.DeviceId import kotlinx.collections.immutable.persistentMapOf -open class DeveloperSettingsStateProvider : PreviewParameterProvider { +open class DeveloperSettingsStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aDeveloperSettingsState(), @@ -42,7 +42,7 @@ fun aDeveloperSettingsState( isEnterpriseBuild: Boolean = false, showColorPicker: Boolean = false, deviceId: DeviceId = DeviceId("ILAKNDNASDLK"), - eventSink: (DeveloperSettingsEvents) -> Unit = {}, + eventSink: (DeveloperSettingsEvent) -> Unit = {}, ) = DeveloperSettingsState( appDeveloperSettingsState = appDeveloperSettingsState, cacheSize = AsyncData.Success("1.2 MB"), diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt index 2fd2137451..e143f42b0b 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt @@ -56,8 +56,8 @@ fun DeveloperSettingsView( title = "Are you sure you want to mark all the rooms as read?", content = "", submitText = stringResource(CommonStrings.action_yes), - onSubmitClick = { state.eventSink(DeveloperSettingsEvents.MarkAllRoomsAsRead(needsConfirmation = false)) }, - onDismiss = { state.eventSink(DeveloperSettingsEvents.DismissMarkAllRoomsAsReadConfirmation) }, + onSubmitClick = { state.eventSink(DeveloperSettingsEvent.MarkAllRoomsAsRead(needsConfirmation = false)) }, + onDismiss = { state.eventSink(DeveloperSettingsEvent.DismissMarkAllRoomsAsReadConfirmation) }, ) } BackHandler( @@ -89,7 +89,7 @@ fun DeveloperSettingsView( Text("Change brand color") }, onClick = { - state.eventSink(DeveloperSettingsEvents.SetShowColorPicker(true)) + state.eventSink(DeveloperSettingsEvent.SetShowColorPicker(true)) } ) ListItem( @@ -97,7 +97,7 @@ fun DeveloperSettingsView( Text("Reset brand color") }, onClick = { - state.eventSink(DeveloperSettingsEvents.ChangeBrandColor(null)) + state.eventSink(DeveloperSettingsEvent.ChangeBrandColor(null)) } ) } @@ -131,7 +131,7 @@ fun DeveloperSettingsView( Text("Vacuum stores") }, onClick = { - state.eventSink(DeveloperSettingsEvents.VacuumStores) + state.eventSink(DeveloperSettingsEvent.VacuumStores) } ) ListItem( @@ -152,7 +152,7 @@ fun DeveloperSettingsView( }, onClick = { if (state.clearCacheAction.isLoading().not()) { - state.eventSink(DeveloperSettingsEvents.ClearCache) + state.eventSink(DeveloperSettingsEvent.ClearCache) } } ) @@ -164,10 +164,10 @@ fun DeveloperSettingsView( showAlphaBar = false, ), onDismissRequest = { - state.eventSink(DeveloperSettingsEvents.SetShowColorPicker(false)) + state.eventSink(DeveloperSettingsEvent.SetShowColorPicker(false)) }, onPickedColor = { - state.eventSink(DeveloperSettingsEvents.ChangeBrandColor(it)) + state.eventSink(DeveloperSettingsEvent.ChangeBrandColor(it)) }, ) } @@ -210,7 +210,7 @@ private fun MarkAllRoomsAsReadCategory(state: DeveloperSettingsState) { }, enabled = !state.showLoader, onClick = { - state.eventSink(DeveloperSettingsEvents.MarkAllRoomsAsRead(needsConfirmation = true)) + state.eventSink(DeveloperSettingsEvent.MarkAllRoomsAsRead(needsConfirmation = true)) }, ) } @@ -231,7 +231,7 @@ private fun NotificationCategory(onPushHistoryClick: () -> Unit) { @PreviewsDayNight @Composable internal fun DeveloperSettingsViewPreview( - @PreviewParameter(DeveloperSettingsStateProvider::class) state: DeveloperSettingsState + @PreviewParameter(DeveloperSettingsStatePreviewParam::class) state: DeveloperSettingsState ) = ElementPreview { DeveloperSettingsView( state = state, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsStatePreviewParam.kt similarity index 94% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsStateProvider.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsStatePreviewParam.kt index e4536f4dba..480b8426d7 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsStatePreviewParam.kt @@ -15,7 +15,7 @@ import io.element.android.libraries.featureflag.ui.model.aFeatureUiModelList import io.element.android.libraries.matrix.api.tracing.TraceLogPack import kotlinx.collections.immutable.toImmutableList -open class AppDeveloperSettingsStateProvider : PreviewParameterProvider { +open class AppDeveloperSettingsStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( anAppDeveloperSettingsState(), diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsView.kt index a4f41edb4a..7b1401b5e5 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/appsettings/AppDeveloperSettingsView.kt @@ -182,7 +182,7 @@ private fun FeatureListContent( @PreviewsDayNight @Composable internal fun AppDeveloperSettingsViewPreview( - @PreviewParameter(AppDeveloperSettingsStateProvider::class) state: AppDeveloperSettingsState + @PreviewParameter(AppDeveloperSettingsStatePreviewParam::class) state: AppDeveloperSettingsState ) = ElementPreview { AppDeveloperSettingsView( state = state, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/di/DeveloperSettingsModule.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/di/DeveloperSettingsBindingContainer.kt similarity index 94% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/di/DeveloperSettingsModule.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/di/DeveloperSettingsBindingContainer.kt index bad0ccae0f..c95b608853 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/di/DeveloperSettingsModule.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/di/DeveloperSettingsBindingContainer.kt @@ -17,7 +17,7 @@ import io.element.android.libraries.architecture.Presenter @ContributesTo(AppScope::class) @BindingContainer -interface DeveloperSettingsModule { +interface DeveloperSettingsBindingContainer { @Binds fun bindAppDeveloperSettingsPresenter(presenter: AppDeveloperSettingsPresenter): Presenter } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsEvent.kt similarity index 92% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsEvents.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsEvent.kt index bc948da09c..e2c60327eb 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsEvent.kt @@ -10,6 +10,6 @@ package io.element.android.features.preferences.impl.labs import io.element.android.libraries.featureflag.ui.model.FeatureUiModel -sealed interface LabsEvents { - data class ToggleFeature(val feature: FeatureUiModel) : LabsEvents +sealed interface LabsEvent { + data class ToggleFeature(val feature: FeatureUiModel) : LabsEvent } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsPresenter.kt index 3b7d499ecf..f9576f93e4 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsPresenter.kt @@ -54,9 +54,9 @@ class LabsPresenter( var isApplyingChanges by remember { mutableStateOf(false) } val featureUiModels = createUiModels(enabledFeatures) - fun handleEvent(event: LabsEvents) { + fun handleEvent(event: LabsEvent) { when (event) { - is LabsEvents.ToggleFeature -> coroutineScope.launch { + is LabsEvent.ToggleFeature -> coroutineScope.launch { val featureIndex = enabledFeatures.indexOfFirst { it.feature.key == event.feature.key }.takeIf { it != -1 } ?: return@launch val enabledFeature = enabledFeatures[featureIndex] val feature = enabledFeature.feature diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsState.kt index 42e70d58c9..55c20d1b43 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsState.kt @@ -14,5 +14,5 @@ import kotlinx.collections.immutable.ImmutableList data class LabsState( val features: ImmutableList, val isApplyingChanges: Boolean, - val eventSink: (LabsEvents) -> Unit, + val eventSink: (LabsEvent) -> Unit, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsStatePreviewParam.kt similarity index 95% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsStateProvider.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsStatePreviewParam.kt index 08e251dcfa..f0b5705376 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsStatePreviewParam.kt @@ -14,7 +14,7 @@ import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.featureflag.ui.model.FeatureUiModel import kotlinx.collections.immutable.toImmutableList -internal class LabsStateProvider : PreviewParameterProvider { +internal class LabsStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aLabsState(features = aFeatureList()), diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsView.kt index 7a6703e3bc..c518b346eb 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/labs/LabsView.kt @@ -90,7 +90,7 @@ fun LabsView( supportingText = feature.description, value = feature.isEnabled, onChange = { - state.eventSink(LabsEvents.ToggleFeature(feature)) + state.eventSink(LabsEvent.ToggleFeature(feature)) } ) } @@ -101,7 +101,7 @@ fun LabsView( @PreviewsDayNight @Composable -internal fun LabsViewPreview(@PreviewParameter(LabsStateProvider::class) state: LabsState) { +internal fun LabsViewPreview(@PreviewParameter(LabsStatePreviewParam::class) state: LabsState) { ElementPreview { LabsView(state = state, onBack = {}) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvent.kt similarity index 79% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvent.kt index 5ca7fc9fdf..fc175f4c0b 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsEvent.kt @@ -10,28 +10,28 @@ package io.element.android.features.preferences.impl.notifications import io.element.android.libraries.preferences.api.store.NotificationSound -sealed interface NotificationSettingsEvents { - data object RefreshSystemNotificationsEnabled : NotificationSettingsEvents - data class SetNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents - data class SetAtRoomNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents - data class SetCallNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents - data class SetInviteForMeNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvents - data object FixConfigurationMismatch : NotificationSettingsEvents - data object ClearConfigurationMismatchError : NotificationSettingsEvents - data object ClearNotificationChangeError : NotificationSettingsEvents - data object ChangePushProvider : NotificationSettingsEvents - data object CancelChangePushProvider : NotificationSettingsEvents - data class SetPushProvider(val index: Int) : NotificationSettingsEvents - data class SetMessageSound(val sound: NotificationSound) : NotificationSettingsEvents - data class SetCallRingtone(val sound: NotificationSound) : NotificationSettingsEvents - data object DismissMessageSoundCopyError : NotificationSettingsEvents - data object DismissCallRingtoneCopyError : NotificationSettingsEvents - data object ShowMessageSoundDialog : NotificationSettingsEvents - data object DismissMessageSoundDialog : NotificationSettingsEvents - data class SelectMessageSoundPreset(val sound: NotificationSound) : NotificationSettingsEvents - data object LaunchMessageSoundPicker : NotificationSettingsEvents - data object ShowCallRingtoneDialog : NotificationSettingsEvents - data object DismissCallRingtoneDialog : NotificationSettingsEvents - data class SelectCallRingtonePreset(val sound: NotificationSound) : NotificationSettingsEvents - data object LaunchCallRingtonePicker : NotificationSettingsEvents +sealed interface NotificationSettingsEvent { + data object RefreshSystemNotificationsEnabled : NotificationSettingsEvent + data class SetNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvent + data class SetAtRoomNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvent + data class SetCallNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvent + data class SetInviteForMeNotificationsEnabled(val enabled: Boolean) : NotificationSettingsEvent + data object FixConfigurationMismatch : NotificationSettingsEvent + data object ClearConfigurationMismatchError : NotificationSettingsEvent + data object ClearNotificationChangeError : NotificationSettingsEvent + data object ChangePushProvider : NotificationSettingsEvent + data object CancelChangePushProvider : NotificationSettingsEvent + data class SetPushProvider(val index: Int) : NotificationSettingsEvent + data class SetMessageSound(val sound: NotificationSound) : NotificationSettingsEvent + data class SetCallRingtone(val sound: NotificationSound) : NotificationSettingsEvent + data object DismissMessageSoundCopyError : NotificationSettingsEvent + data object DismissCallRingtoneCopyError : NotificationSettingsEvent + data object ShowMessageSoundDialog : NotificationSettingsEvent + data object DismissMessageSoundDialog : NotificationSettingsEvent + data class SelectMessageSoundPreset(val sound: NotificationSound) : NotificationSettingsEvent + data object LaunchMessageSoundPicker : NotificationSettingsEvent + data object ShowCallRingtoneDialog : NotificationSettingsEvent + data object DismissCallRingtoneDialog : NotificationSettingsEvent + data class SelectCallRingtonePreset(val sound: NotificationSound) : NotificationSettingsEvent + data object LaunchCallRingtonePicker : NotificationSettingsEvent } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt index 03fe2cf31b..5256f9ec9c 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenter.kt @@ -196,38 +196,38 @@ class NotificationSettingsPresenter( ) } - fun handleEvent(event: NotificationSettingsEvents) { + fun handleEvent(event: NotificationSettingsEvent) { when (event) { - is NotificationSettingsEvents.SetAtRoomNotificationsEnabled -> { + is NotificationSettingsEvent.SetAtRoomNotificationsEnabled -> { localCoroutineScope.setAtRoomNotificationsEnabled(event.enabled, changeNotificationSettingAction) } - is NotificationSettingsEvents.SetCallNotificationsEnabled -> { + is NotificationSettingsEvent.SetCallNotificationsEnabled -> { localCoroutineScope.setCallNotificationsEnabled(event.enabled, changeNotificationSettingAction) } - is NotificationSettingsEvents.SetInviteForMeNotificationsEnabled -> { + is NotificationSettingsEvent.SetInviteForMeNotificationsEnabled -> { localCoroutineScope.setInviteForMeNotificationsEnabled(event.enabled, changeNotificationSettingAction) } - is NotificationSettingsEvents.SetNotificationsEnabled -> sessionCoroutineScope.setNotificationsEnabled(userPushStore, event.enabled) - NotificationSettingsEvents.ClearConfigurationMismatchError -> { + is NotificationSettingsEvent.SetNotificationsEnabled -> sessionCoroutineScope.setNotificationsEnabled(userPushStore, event.enabled) + NotificationSettingsEvent.ClearConfigurationMismatchError -> { matrixSettings.value = NotificationSettingsState.MatrixSettings.Invalid(fixFailed = false) } - NotificationSettingsEvents.FixConfigurationMismatch -> localCoroutineScope.fixConfigurationMismatch(matrixSettings) - NotificationSettingsEvents.RefreshSystemNotificationsEnabled -> { + NotificationSettingsEvent.FixConfigurationMismatch -> localCoroutineScope.fixConfigurationMismatch(matrixSettings) + NotificationSettingsEvent.RefreshSystemNotificationsEnabled -> { systemNotificationsEnabled.value = systemNotificationsEnabledProvider.notificationsEnabled() refreshFullScreenIntentSettings++ } - NotificationSettingsEvents.ClearNotificationChangeError -> changeNotificationSettingAction.value = AsyncAction.Uninitialized - NotificationSettingsEvents.ChangePushProvider -> showChangePushProviderDialog = true - NotificationSettingsEvents.CancelChangePushProvider -> showChangePushProviderDialog = false - is NotificationSettingsEvents.SetPushProvider -> localCoroutineScope.changePushProvider(distributors.getOrNull(event.index)) - is NotificationSettingsEvents.SetMessageSound -> applyMessageSound( + NotificationSettingsEvent.ClearNotificationChangeError -> changeNotificationSettingAction.value = AsyncAction.Uninitialized + NotificationSettingsEvent.ChangePushProvider -> showChangePushProviderDialog = true + NotificationSettingsEvent.CancelChangePushProvider -> showChangePushProviderDialog = false + is NotificationSettingsEvent.SetPushProvider -> localCoroutineScope.changePushProvider(distributors.getOrNull(event.index)) + is NotificationSettingsEvent.SetMessageSound -> applyMessageSound( sound = event.sound, sessionCoroutineScope = sessionCoroutineScope, onCopyError = { messageSoundCopyError = true }, onCopySuccess = { messageSoundCopyError = false }, onChannelFailure = { failure -> changeNotificationSettingAction.value = AsyncAction.Failure(failure) }, ) - is NotificationSettingsEvents.SelectMessageSoundPreset -> { + is NotificationSettingsEvent.SelectMessageSoundPreset -> { showMessageSoundDialog = false applyMessageSound( sound = event.sound, @@ -237,13 +237,13 @@ class NotificationSettingsPresenter( onChannelFailure = { failure -> changeNotificationSettingAction.value = AsyncAction.Failure(failure) }, ) } - NotificationSettingsEvents.ShowMessageSoundDialog -> showMessageSoundDialog = true - NotificationSettingsEvents.DismissMessageSoundDialog -> showMessageSoundDialog = false - NotificationSettingsEvents.LaunchMessageSoundPicker -> { + NotificationSettingsEvent.ShowMessageSoundDialog -> showMessageSoundDialog = true + NotificationSettingsEvent.DismissMessageSoundDialog -> showMessageSoundDialog = false + NotificationSettingsEvent.LaunchMessageSoundPicker -> { showMessageSoundDialog = false pendingMessageSoundPickerLaunch++ } - is NotificationSettingsEvents.SetCallRingtone -> { + is NotificationSettingsEvent.SetCallRingtone -> { legacyCallRingtone = null applyCallRingtone( sound = event.sound, @@ -253,7 +253,7 @@ class NotificationSettingsPresenter( onChannelFailure = { failure -> changeNotificationSettingAction.value = AsyncAction.Failure(failure) }, ) } - is NotificationSettingsEvents.SelectCallRingtonePreset -> { + is NotificationSettingsEvent.SelectCallRingtonePreset -> { showCallRingtoneDialog = false legacyCallRingtone = null applyCallRingtone( @@ -264,16 +264,16 @@ class NotificationSettingsPresenter( onChannelFailure = { failure -> changeNotificationSettingAction.value = AsyncAction.Failure(failure) }, ) } - NotificationSettingsEvents.ShowCallRingtoneDialog -> showCallRingtoneDialog = true - NotificationSettingsEvents.DismissCallRingtoneDialog -> showCallRingtoneDialog = false - NotificationSettingsEvents.LaunchCallRingtonePicker -> { + NotificationSettingsEvent.ShowCallRingtoneDialog -> showCallRingtoneDialog = true + NotificationSettingsEvent.DismissCallRingtoneDialog -> showCallRingtoneDialog = false + NotificationSettingsEvent.LaunchCallRingtonePicker -> { showCallRingtoneDialog = false pendingCallRingtonePickerLaunch++ } - NotificationSettingsEvents.DismissMessageSoundCopyError -> { + NotificationSettingsEvent.DismissMessageSoundCopyError -> { messageSoundCopyError = false } - NotificationSettingsEvents.DismissCallRingtoneCopyError -> { + NotificationSettingsEvent.DismissCallRingtoneCopyError -> { callRingtoneCopyError = false } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt index 3c59f6c355..379b8589b7 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsState.kt @@ -30,7 +30,7 @@ data class NotificationSettingsState( val showCallRingtoneDialog: Boolean, /** * One-shot trigger for launching the system ringtone picker. Each - * [NotificationSettingsEvents.LaunchMessageSoundPicker] increments the value; + * [NotificationSettingsEvent.LaunchMessageSoundPicker] increments the value; * the view watches this in `LaunchedEffect` and calls the launcher whenever it goes above 0. * **Always start at 0** in initial state, providers, and previews — a non-zero seed will * auto-open the picker on screen entry. @@ -42,7 +42,7 @@ data class NotificationSettingsState( * auto-opens on screen entry. */ val pendingCallRingtonePickerLaunch: Int, - val eventSink: (NotificationSettingsEvents) -> Unit, + val eventSink: (NotificationSettingsEvent) -> Unit, ) { sealed interface MatrixSettings { data object Uninitialized : MatrixSettings diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStatePreviewParam.kt similarity index 97% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStatePreviewParam.kt index 905f869dbe..33f20cdb6e 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsStatePreviewParam.kt @@ -19,7 +19,7 @@ import io.element.android.libraries.pushproviders.api.Distributor import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -open class NotificationSettingsStateProvider : PreviewParameterProvider { +open class NotificationSettingsStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aValidNotificationSettingsState(systemNotificationsEnabled = false), @@ -101,7 +101,7 @@ fun aValidNotificationSettingsState( showCallRingtoneDialog: Boolean = false, pendingMessageSoundPickerLaunch: Int = 0, pendingCallRingtonePickerLaunch: Int = 0, - eventSink: (NotificationSettingsEvents) -> Unit = {}, + eventSink: (NotificationSettingsEvent) -> Unit = {}, ) = NotificationSettingsState( matrixSettings = NotificationSettingsState.MatrixSettings.Valid( atRoomNotificationsEnabled = atRoomNotificationsEnabled, @@ -138,7 +138,7 @@ fun aValidNotificationSettingsState( fun aInvalidNotificationSettingsState( fixFailed: Boolean = false, - eventSink: (NotificationSettingsEvents) -> Unit = {}, + eventSink: (NotificationSettingsEvent) -> Unit = {}, ) = NotificationSettingsState( matrixSettings = NotificationSettingsState.MatrixSettings.Invalid( fixFailed = fixFailed, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt index 9ede5138cf..21f0cc7b51 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsView.kt @@ -54,7 +54,7 @@ import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.OnLifecycleEvent -import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvents +import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvent import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.preferences.api.store.NotificationSound import io.element.android.libraries.ui.strings.CommonStrings @@ -74,7 +74,7 @@ fun NotificationSettingsView( ) { OnLifecycleEvent { _, event -> when (event) { - Lifecycle.Event.ON_RESUME -> state.eventSink.invoke(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) + Lifecycle.Event.ON_RESUME -> state.eventSink.invoke(NotificationSettingsEvent.RefreshSystemNotificationsEnabled) else -> Unit } } @@ -86,27 +86,27 @@ fun NotificationSettingsView( when (state.matrixSettings) { is NotificationSettingsState.MatrixSettings.Invalid -> InvalidNotificationSettingsView( showError = state.matrixSettings.fixFailed, - onContinueClick = { state.eventSink(NotificationSettingsEvents.FixConfigurationMismatch) }, - onDismissError = { state.eventSink(NotificationSettingsEvents.ClearConfigurationMismatchError) }, + onContinueClick = { state.eventSink(NotificationSettingsEvent.FixConfigurationMismatch) }, + onDismissError = { state.eventSink(NotificationSettingsEvent.ClearConfigurationMismatchError) }, ) NotificationSettingsState.MatrixSettings.Uninitialized -> return@PreferencePage is NotificationSettingsState.MatrixSettings.Valid -> NotificationSettingsContentView( matrixSettings = state.matrixSettings, state = state, - onNotificationsEnabledChange = { state.eventSink(NotificationSettingsEvents.SetNotificationsEnabled(it)) }, + onNotificationsEnabledChange = { state.eventSink(NotificationSettingsEvent.SetNotificationsEnabled(it)) }, onGroupChatsClick = { onOpenEditDefault(false) }, onDirectChatsClick = { onOpenEditDefault(true) }, - onMentionNotificationsChange = { state.eventSink(NotificationSettingsEvents.SetAtRoomNotificationsEnabled(it)) }, + onMentionNotificationsChange = { state.eventSink(NotificationSettingsEvent.SetAtRoomNotificationsEnabled(it)) }, // TODO We are removing the call notification toggle until support for call notifications has been added -// onCallsNotificationsChanged = { state.eventSink(NotificationSettingsEvents.SetCallNotificationsEnabled(it)) }, - onInviteForMeNotificationsChange = { state.eventSink(NotificationSettingsEvents.SetInviteForMeNotificationsEnabled(it)) }, +// onCallsNotificationsChanged = { state.eventSink(NotificationSettingsEvent.SetCallNotificationsEnabled(it)) }, + onInviteForMeNotificationsChange = { state.eventSink(NotificationSettingsEvent.SetInviteForMeNotificationsEnabled(it)) }, onTroubleshootNotificationsClick = onTroubleshootNotificationsClick, ) } AsyncActionView( async = state.changeNotificationSettingAction, errorMessage = { stringResource(R.string.screen_notification_settings_edit_failed_updating_default_mode) }, - onErrorDismiss = { state.eventSink(NotificationSettingsEvents.ClearNotificationChangeError) }, + onErrorDismiss = { state.eventSink(NotificationSettingsEvent.ClearNotificationChangeError) }, onSuccess = {}, ) } @@ -165,7 +165,7 @@ private fun NotificationSettingsContentView( Text(stringResource(R.string.full_screen_intent_banner_message)) }, onClick = { - state.fullScreenIntentPermissionsState.eventSink(FullScreenIntentPermissionsEvents.OpenSettings) + state.fullScreenIntentPermissionsState.eventSink(FullScreenIntentPermissionsEvent.OpenSettings) } ) } @@ -249,7 +249,7 @@ private fun NotificationSettingsContentView( }, onClick = { if (state.currentPushDistributor.isReady()) { - state.eventSink(NotificationSettingsEvents.ChangePushProvider) + state.eventSink(NotificationSettingsEvent.ChangePushProvider) } } ) @@ -269,10 +269,10 @@ private fun NotificationSettingsContentView( initialSelection = state.availablePushDistributors.indexOf(state.currentPushDistributor.dataOrNull()), onSelectOption = { index -> state.eventSink( - NotificationSettingsEvents.SetPushProvider(index) + NotificationSettingsEvent.SetPushProvider(index) ) }, - onDismissRequest = { state.eventSink(NotificationSettingsEvents.CancelChangePushProvider) }, + onDismissRequest = { state.eventSink(NotificationSettingsEvent.CancelChangePushProvider) }, ) } } @@ -286,7 +286,7 @@ private fun SoundsPreferenceCategory(state: NotificationSettingsState) { type = RingtoneManager.TYPE_NOTIFICATION, current = state.messageSound.sound, defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI, - onSoundPick = { sound -> state.eventSink(NotificationSettingsEvents.SetMessageSound(sound)) }, + onSoundPick = { sound -> state.eventSink(NotificationSettingsEvent.SetMessageSound(sound)) }, ) // Skip the initial 0 emission so the picker doesn't auto-open on screen entry; only // increments fired by LaunchMessageSoundPicker should launch it. @@ -298,11 +298,11 @@ private fun SoundsPreferenceCategory(state: NotificationSettingsState) { ListItem( content = { Text(stringResource(id = R.string.screen_notification_settings_message_sound_label)) }, supportingContent = { Text(state.messageSound.displayName) }, - onClick = { state.eventSink(NotificationSettingsEvents.ShowMessageSoundDialog) }, + onClick = { state.eventSink(NotificationSettingsEvent.ShowMessageSoundDialog) }, ) if (state.messageSound.copyError) { SoundCopyErrorRow( - onDismissClick = { state.eventSink(NotificationSettingsEvents.DismissMessageSoundCopyError) }, + onDismissClick = { state.eventSink(NotificationSettingsEvent.DismissMessageSoundCopyError) }, ) } if (state.showMessageSoundDialog) { @@ -313,7 +313,7 @@ private fun SoundsPreferenceCategory(state: NotificationSettingsState) { type = RingtoneManager.TYPE_RINGTONE, current = state.callRingtone.sound, defaultUri = Settings.System.DEFAULT_RINGTONE_URI, - onSoundPick = { sound -> state.eventSink(NotificationSettingsEvents.SetCallRingtone(sound)) }, + onSoundPick = { sound -> state.eventSink(NotificationSettingsEvent.SetCallRingtone(sound)) }, ) // Skip the initial 0 emission so the picker doesn't auto-open on screen entry; only // increments fired by LaunchCallRingtonePicker should launch it. @@ -325,11 +325,11 @@ private fun SoundsPreferenceCategory(state: NotificationSettingsState) { ListItem( content = { Text(stringResource(id = R.string.screen_notification_settings_call_ringtone_label)) }, supportingContent = { Text(state.callRingtone.displayName) }, - onClick = { state.eventSink(NotificationSettingsEvents.ShowCallRingtoneDialog) }, + onClick = { state.eventSink(NotificationSettingsEvent.ShowCallRingtoneDialog) }, ) if (state.callRingtone.copyError) { SoundCopyErrorRow( - onDismissClick = { state.eventSink(NotificationSettingsEvents.DismissCallRingtoneCopyError) }, + onDismissClick = { state.eventSink(NotificationSettingsEvent.DismissCallRingtoneCopyError) }, ) } if (state.showCallRingtoneDialog) { @@ -368,13 +368,13 @@ private fun MessageSoundDialog(state: NotificationSettingsState) { initialSelection = initialSelection, onSelectOption = { index -> when (index) { - 0 -> state.eventSink(NotificationSettingsEvents.SelectMessageSoundPreset(NotificationSound.ElementDefault)) - 1 -> state.eventSink(NotificationSettingsEvents.SelectMessageSoundPreset(NotificationSound.ElementFade)) - 2 -> state.eventSink(NotificationSettingsEvents.SelectMessageSoundPreset(NotificationSound.SystemDefault)) - else -> state.eventSink(NotificationSettingsEvents.LaunchMessageSoundPicker) + 0 -> state.eventSink(NotificationSettingsEvent.SelectMessageSoundPreset(NotificationSound.ElementDefault)) + 1 -> state.eventSink(NotificationSettingsEvent.SelectMessageSoundPreset(NotificationSound.ElementFade)) + 2 -> state.eventSink(NotificationSettingsEvent.SelectMessageSoundPreset(NotificationSound.SystemDefault)) + else -> state.eventSink(NotificationSettingsEvent.LaunchMessageSoundPicker) } }, - onDismissRequest = { state.eventSink(NotificationSettingsEvents.DismissMessageSoundDialog) }, + onDismissRequest = { state.eventSink(NotificationSettingsEvent.DismissMessageSoundDialog) }, ) } @@ -402,11 +402,11 @@ private fun CallRingtoneDialog(state: NotificationSettingsState) { initialSelection = initialSelection, onSelectOption = { index -> when (index) { - 0 -> state.eventSink(NotificationSettingsEvents.SelectCallRingtonePreset(NotificationSound.SystemDefault)) - else -> state.eventSink(NotificationSettingsEvents.LaunchCallRingtonePicker) + 0 -> state.eventSink(NotificationSettingsEvent.SelectCallRingtonePreset(NotificationSound.SystemDefault)) + else -> state.eventSink(NotificationSettingsEvent.LaunchCallRingtonePicker) } }, - onDismissRequest = { state.eventSink(NotificationSettingsEvents.DismissCallRingtoneDialog) }, + onDismissRequest = { state.eventSink(NotificationSettingsEvent.DismissCallRingtoneDialog) }, ) } @@ -500,7 +500,9 @@ private fun InvalidNotificationSettingsView( @PreviewsDayNight @Composable -internal fun NotificationSettingsViewPreview(@PreviewParameter(NotificationSettingsStateProvider::class) state: NotificationSettingsState) = ElementPreview { +internal fun NotificationSettingsViewPreview(@PreviewParameter( + NotificationSettingsStatePreviewParam::class +) state: NotificationSettingsState) = ElementPreview { NotificationSettingsView( state = state, onBackClick = {}, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt index f2cb8b02cc..5769a8abe2 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingPresenter.kt @@ -76,12 +76,12 @@ class EditDefaultNotificationSettingPresenter( displayMentionsOnlyDisclaimer = !notificationSettingsService.canHomeServerPushEncryptedEventsToDevice().getOrDefault(true) } - fun handleEvent(event: EditDefaultNotificationSettingStateEvents) { + fun handleEvent(event: EditDefaultNotificationSettingStateEvent) { when (event) { - is EditDefaultNotificationSettingStateEvents.SetNotificationMode -> { + is EditDefaultNotificationSettingStateEvent.SetNotificationMode -> { localCoroutineScope.setDefaultNotificationMode(event.mode, changeNotificationSettingAction) } - EditDefaultNotificationSettingStateEvents.ClearError -> changeNotificationSettingAction.value = AsyncAction.Uninitialized + EditDefaultNotificationSettingStateEvent.ClearError -> changeNotificationSettingAction.value = AsyncAction.Uninitialized } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingState.kt index 17c8cad95c..840f62eb4f 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingState.kt @@ -18,5 +18,5 @@ data class EditDefaultNotificationSettingState( val roomsWithUserDefinedMode: ImmutableList, val changeNotificationSettingAction: AsyncAction, val displayMentionsOnlyDisclaimer: Boolean, - val eventSink: (EditDefaultNotificationSettingStateEvents) -> Unit, + val eventSink: (EditDefaultNotificationSettingStateEvent) -> Unit, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateEvent.kt similarity index 81% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateEvents.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateEvent.kt index a7993c118a..4ede5813c1 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateEvent.kt @@ -10,7 +10,7 @@ package io.element.android.features.preferences.impl.notifications.edit import io.element.android.libraries.matrix.api.room.RoomNotificationMode -sealed interface EditDefaultNotificationSettingStateEvents { - data class SetNotificationMode(val mode: RoomNotificationMode) : EditDefaultNotificationSettingStateEvents - data object ClearError : EditDefaultNotificationSettingStateEvents +sealed interface EditDefaultNotificationSettingStateEvent { + data class SetNotificationMode(val mode: RoomNotificationMode) : EditDefaultNotificationSettingStateEvent + data object ClearError : EditDefaultNotificationSettingStateEvent } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStatePreviewParam.kt similarity index 95% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateProvider.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStatePreviewParam.kt index 389554d2e0..819cd0d1f3 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingStatePreviewParam.kt @@ -16,7 +16,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomNotificationMode import kotlinx.collections.immutable.persistentListOf -open class EditDefaultNotificationSettingStateProvider : PreviewParameterProvider { +open class EditDefaultNotificationSettingStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( anEditDefaultNotificationSettingsState(), diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt index 24da141a48..9bfde12513 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/notifications/edit/EditDefaultNotificationSettingView.kt @@ -70,7 +70,7 @@ fun EditDefaultNotificationSettingView( mode = item, isSelected = state.mode == item, displayMentionsOnlyDisclaimer = state.displayMentionsOnlyDisclaimer, - onSelectOption = { state.eventSink(EditDefaultNotificationSettingStateEvents.SetNotificationMode(it)) } + onSelectOption = { state.eventSink(EditDefaultNotificationSettingStateEvent.SetNotificationMode(it)) } ) } } @@ -116,7 +116,7 @@ fun EditDefaultNotificationSettingView( AsyncActionView( async = state.changeNotificationSettingAction, errorMessage = { stringResource(R.string.screen_notification_settings_edit_failed_updating_default_mode) }, - onErrorDismiss = { state.eventSink(EditDefaultNotificationSettingStateEvents.ClearError) }, + onErrorDismiss = { state.eventSink(EditDefaultNotificationSettingStateEvent.ClearError) }, onSuccess = {}, ) } @@ -125,7 +125,7 @@ fun EditDefaultNotificationSettingView( @PreviewsDayNight @Composable internal fun EditDefaultNotificationSettingViewPreview( - @PreviewParameter(EditDefaultNotificationSettingStateProvider::class) state: EditDefaultNotificationSettingState + @PreviewParameter(EditDefaultNotificationSettingStatePreviewParam::class) state: EditDefaultNotificationSettingState ) = ElementPreview { EditDefaultNotificationSettingView( state = state, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt index 8ee72fba6d..4fdac07550 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootNode.kt @@ -19,7 +19,7 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.compound.theme.ElementTheme -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.DirectLogoutView import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.architecture.callback @@ -96,7 +96,7 @@ class PreferencesRootNode( onOpenBlockedUsers = callback::navigateToBlockedUsers, onSignOutClick = { if (state.directLogoutState.canDoDirectSignOut) { - state.directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false)) + state.directLogoutState.eventSink(DirectLogoutEvent.Logout(ignoreSdkError = false)) } else { callback.startSignOutFlow() } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt index 679e033d9b..3a720fc7a8 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt @@ -76,10 +76,7 @@ class PreferencesRootPresenter( }.collectAsState(initial = false) val isUserStatusSupported by produceState(false) { - featureFlagService.isFeatureEnabledFlow(FeatureFlags.UserStatus) - .collect { isUserStatusEnabled -> - value = isUserStatusEnabled && matrixClient.isUserStatusSupported().getOrDefault(false) - } + value = matrixClient.isUserStatusSupported().getOrDefault(false) } val userStatusState = if (isUserStatusSupported) userStatusPresenter.present() else null diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStatePreviewParam.kt similarity index 98% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStatePreviewParam.kt index 86f984f254..80c47a33b2 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStatePreviewParam.kt @@ -21,7 +21,7 @@ import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.toImmutableList -open class PreferencesRootStateProvider : PreviewParameterProvider { +open class PreferencesRootStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( // Nominal state, that a regular user will see if multi account is enabled diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt index 2a37309e26..82c88b16fa 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt @@ -373,14 +373,14 @@ private fun DeveloperPreferencesView(onOpenDeveloperSettings: () -> Unit) { @PreviewWithLargeHeight @Composable -internal fun PreferencesRootViewLightPreview(@PreviewParameter(PreferencesRootStateProvider::class) state: PreferencesRootState) = +internal fun PreferencesRootViewLightPreview(@PreviewParameter(PreferencesRootStatePreviewParam::class) state: PreferencesRootState) = ElementPreviewLight( drawableFallbackForImages = CommonDrawables.sample_avatar, ) { ContentToPreview(state) } @PreviewWithLargeHeight @Composable -internal fun PreferencesRootViewDarkPreview(@PreviewParameter(PreferencesRootStateProvider::class) state: PreferencesRootState) = +internal fun PreferencesRootViewDarkPreview(@PreviewParameter(PreferencesRootStatePreviewParam::class) state: PreferencesRootState) = ElementPreviewDark( drawableFallbackForImages = CommonDrawables.sample_avatar, ) { ContentToPreview(state) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/UserPreferences.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/UserPreferences.kt index 43c7e8dacf..066b9ac73e 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/UserPreferences.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/UserPreferences.kt @@ -15,7 +15,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.MatrixUserHeader -import io.element.android.libraries.matrix.ui.components.MatrixUserProvider +import io.element.android.libraries.matrix.ui.components.MatrixUserPreviewParam @Composable fun UserPreferences( @@ -30,6 +30,6 @@ fun UserPreferences( @PreviewsDayNight @Composable -internal fun UserPreferencesPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = ElementPreview { +internal fun UserPreferencesPreview(@PreviewParameter(MatrixUserPreviewParam::class) matrixUser: MatrixUser) = ElementPreview { UserPreferences(matrixUser) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStatePreviewParam.kt similarity index 95% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStatePreviewParam.kt index 2f238f9935..8b711cba51 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileStatePreviewParam.kt @@ -17,7 +17,7 @@ import io.element.android.libraries.permissions.api.PermissionsState import io.element.android.libraries.permissions.api.aPermissionsState import kotlinx.collections.immutable.toImmutableList -open class EditUserProfileStateProvider : PreviewParameterProvider { +open class EditUserProfileStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aEditUserProfileState(), diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt index d4571d7be5..89e739fe56 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/user/editprofile/EditUserProfileView.kt @@ -178,7 +178,7 @@ fun EditUserProfileView( @PreviewsDayNight @Composable -internal fun EditUserProfileViewPreview(@PreviewParameter(EditUserProfileStateProvider::class) state: EditUserProfileState) = +internal fun EditUserProfileViewPreview(@PreviewParameter(EditUserProfileStatePreviewParam::class) state: EditUserProfileState) = ElementPreview { EditUserProfileView( onEditProfileSuccess = {}, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusStatePreviewParam.kt similarity index 96% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusStateProvider.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusStatePreviewParam.kt index 6b198183a7..34a12c604e 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusStatePreviewParam.kt @@ -13,7 +13,7 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.matrix.api.user.UserStatus -internal class UserStatusStateProvider : PreviewParameterProvider { +internal class UserStatusStatePreviewParam : PreviewParameterProvider { override val values get() = sequenceOf( aUserStatusState(), aUserStatusState(displayedStatus = DisplayedStatus.UserSet(UserStatus("🌴", "Away"))), diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusView.kt index 19cc43d3d7..c25c159f33 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusView.kt @@ -354,7 +354,7 @@ private fun EmojiPickerBottomSheet( @PreviewsDayNight @Composable -internal fun UserStatusViewPreview(@PreviewParameter(UserStatusStateProvider::class) state: UserStatusState) = ElementPreview { +internal fun UserStatusViewPreview(@PreviewParameter(UserStatusStatePreviewParam::class) state: UserStatusState) = ElementPreview { UserStatusView( state = state, emojiPickerRenderer = NoOpEmojiPickerRenderer, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusBindingContainer.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/di/UserStatusBindingContainer.kt similarity index 73% rename from features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusBindingContainer.kt rename to features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/di/UserStatusBindingContainer.kt index dfa77483f8..0e6f1bdc2f 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/UserStatusBindingContainer.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/userstatus/di/UserStatusBindingContainer.kt @@ -5,11 +5,13 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.features.preferences.impl.userstatus +package io.element.android.features.preferences.impl.userstatus.di import dev.zacsweers.metro.BindingContainer import dev.zacsweers.metro.Binds import dev.zacsweers.metro.ContributesTo +import io.element.android.features.preferences.impl.userstatus.UserStatusPresenter +import io.element.android.features.preferences.impl.userstatus.UserStatusState import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.SessionScope diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt index 25d7c01778..2ee594e8b9 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt @@ -71,11 +71,11 @@ class AdvancedSettingsPresenterTest { with(awaitItem()) { assertThat(isDeveloperModeEnabled).isFalse() - eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(true)) + eventSink(AdvancedSettingsEvent.SetDeveloperModeEnabled(true)) } with(awaitItem()) { assertThat(isDeveloperModeEnabled).isTrue() - eventSink(AdvancedSettingsEvents.SetDeveloperModeEnabled(false)) + eventSink(AdvancedSettingsEvent.SetDeveloperModeEnabled(false)) } with(awaitItem()) { assertThat(isDeveloperModeEnabled).isFalse() @@ -94,11 +94,11 @@ class AdvancedSettingsPresenterTest { with(awaitItem()) { assertThat(isSharePresenceEnabled).isTrue() - eventSink(AdvancedSettingsEvents.SetSharePresenceEnabled(false)) + eventSink(AdvancedSettingsEvent.SetSharePresenceEnabled(false)) } with(awaitItem()) { assertThat(isSharePresenceEnabled).isFalse() - eventSink(AdvancedSettingsEvents.SetSharePresenceEnabled(true)) + eventSink(AdvancedSettingsEvent.SetSharePresenceEnabled(true)) } with(awaitItem()) { assertThat(isSharePresenceEnabled).isTrue() @@ -117,11 +117,11 @@ class AdvancedSettingsPresenterTest { with(awaitItem()) { assertThat((mediaOptimizationState as MediaOptimizationState.AllMedia).isEnabled).isTrue() - eventSink(AdvancedSettingsEvents.SetCompressMedia(false)) + eventSink(AdvancedSettingsEvent.SetCompressMedia(false)) } with(awaitItem()) { assertThat((mediaOptimizationState as MediaOptimizationState.AllMedia).isEnabled).isFalse() - eventSink(AdvancedSettingsEvents.SetCompressMedia(true)) + eventSink(AdvancedSettingsEvent.SetCompressMedia(true)) } with(awaitItem()) { assertThat((mediaOptimizationState as MediaOptimizationState.AllMedia).isEnabled).isTrue() @@ -144,11 +144,11 @@ class AdvancedSettingsPresenterTest { with(awaitItem()) { assertThat((mediaOptimizationState as MediaOptimizationState.Split).compressImages).isTrue() - eventSink(AdvancedSettingsEvents.SetCompressImages(false)) + eventSink(AdvancedSettingsEvent.SetCompressImages(false)) } with(awaitItem()) { assertThat((mediaOptimizationState as MediaOptimizationState.Split).compressImages).isFalse() - eventSink(AdvancedSettingsEvents.SetCompressImages(true)) + eventSink(AdvancedSettingsEvent.SetCompressImages(true)) } with(awaitItem()) { assertThat((mediaOptimizationState as MediaOptimizationState.Split).compressImages).isTrue() @@ -171,11 +171,11 @@ class AdvancedSettingsPresenterTest { with(awaitItem()) { assertThat((mediaOptimizationState as MediaOptimizationState.Split).videoPreset).isEqualTo(VideoCompressionPreset.STANDARD) - eventSink(AdvancedSettingsEvents.SetVideoUploadQuality(VideoCompressionPreset.LOW)) + eventSink(AdvancedSettingsEvent.SetVideoUploadQuality(VideoCompressionPreset.LOW)) } with(awaitItem()) { assertThat((mediaOptimizationState as MediaOptimizationState.Split).videoPreset).isEqualTo(VideoCompressionPreset.LOW) - eventSink(AdvancedSettingsEvents.SetVideoUploadQuality(VideoCompressionPreset.HIGH)) + eventSink(AdvancedSettingsEvent.SetVideoUploadQuality(VideoCompressionPreset.HIGH)) } with(awaitItem()) { assertThat((mediaOptimizationState as MediaOptimizationState.Split).videoPreset).isEqualTo(VideoCompressionPreset.HIGH) @@ -194,15 +194,15 @@ class AdvancedSettingsPresenterTest { with(awaitItem()) { assertThat(theme).isEqualTo(ThemeOption.System) - eventSink(AdvancedSettingsEvents.SetTheme(ThemeOption.Dark)) + eventSink(AdvancedSettingsEvent.SetTheme(ThemeOption.Dark)) } with(awaitItem()) { assertThat(theme).isEqualTo(ThemeOption.Dark) - eventSink(AdvancedSettingsEvents.SetTheme(ThemeOption.Light)) + eventSink(AdvancedSettingsEvent.SetTheme(ThemeOption.Light)) } with(awaitItem()) { assertThat(theme).isEqualTo(ThemeOption.Light) - eventSink(AdvancedSettingsEvents.SetTheme(ThemeOption.System)) + eventSink(AdvancedSettingsEvent.SetTheme(ThemeOption.System)) } with(awaitItem()) { assertThat(theme).isEqualTo(ThemeOption.System) @@ -242,7 +242,7 @@ class AdvancedSettingsPresenterTest { with(awaitItem()) { assertThat(liveLocationMinimumDistanceUpdate).isEqualTo(10) - eventSink(AdvancedSettingsEvents.SetLiveLocationMinimumDistanceUpdate(42)) + eventSink(AdvancedSettingsEvent.SetLiveLocationMinimumDistanceUpdate(42)) } with(awaitItem()) { assertThat(liveLocationMinimumDistanceUpdate).isEqualTo(42) @@ -298,11 +298,11 @@ class AdvancedSettingsPresenterTest { with(awaitItem()) { assertThat(mediaPreviewConfigState.hideInviteAvatars).isFalse() - eventSink(AdvancedSettingsEvents.SetHideInviteAvatars(true)) + eventSink(AdvancedSettingsEvent.SetHideInviteAvatars(true)) } with(awaitItem()) { assertThat(mediaPreviewConfigState.hideInviteAvatars).isTrue() - eventSink(AdvancedSettingsEvents.SetHideInviteAvatars(false)) + eventSink(AdvancedSettingsEvent.SetHideInviteAvatars(false)) } with(awaitItem()) { assertThat(mediaPreviewConfigState.hideInviteAvatars).isFalse() @@ -323,11 +323,11 @@ class AdvancedSettingsPresenterTest { with(awaitItem()) { assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.On) - eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Off)) + eventSink(AdvancedSettingsEvent.SetTimelineMediaPreviewValue(MediaPreviewValue.Off)) } with(awaitItem()) { assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Off) - eventSink(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Private)) + eventSink(AdvancedSettingsEvent.SetTimelineMediaPreviewValue(MediaPreviewValue.Private)) } with(awaitItem()) { assertThat(mediaPreviewConfigState.timelineMediaPreviewValue).isEqualTo(MediaPreviewValue.Private) diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt index fa4ecfd829..52af7d5927 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt @@ -39,7 +39,7 @@ import org.robolectric.annotation.Config class AdvancedSettingsViewTest : RobolectricTest() { @Test fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { setAdvancedSettingsView( state = aAdvancedSettingsState( @@ -53,7 +53,7 @@ class AdvancedSettingsViewTest : RobolectricTest() { @Test fun `clicking on other theme emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, @@ -61,7 +61,7 @@ class AdvancedSettingsViewTest : RobolectricTest() { ) clickOn(CommonStrings.common_appearance) clickOn(R.string.theme_dark) - eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTheme(ThemeOption.Dark)) + eventsRecorder.assertSingle(AdvancedSettingsEvent.SetTheme(ThemeOption.Dark)) } @Test @@ -91,31 +91,31 @@ class AdvancedSettingsViewTest : RobolectricTest() { @Test fun `clicking on View source emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, ), ) clickOn(CommonStrings.action_view_source) - eventsRecorder.assertSingle(AdvancedSettingsEvents.SetDeveloperModeEnabled(true)) + eventsRecorder.assertSingle(AdvancedSettingsEvent.SetDeveloperModeEnabled(true)) } @Test fun `clicking on Share presence emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, ), ) clickOn(R.string.screen_advanced_settings_share_presence) - eventsRecorder.assertSingle(AdvancedSettingsEvents.SetSharePresenceEnabled(true)) + eventsRecorder.assertSingle(AdvancedSettingsEvent.SetSharePresenceEnabled(true)) } @Test fun `clicking on media to enable compression emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() val analyticsService = FakeAnalyticsService() setAdvancedSettingsView( state = aAdvancedSettingsState( @@ -124,7 +124,7 @@ class AdvancedSettingsViewTest : RobolectricTest() { analyticsService = analyticsService ) clickOn(R.string.screen_advanced_settings_media_compression_description) - eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(true)) + eventsRecorder.assertSingle(AdvancedSettingsEvent.SetCompressMedia(true)) assertThat(analyticsService.capturedEvents).isEqualTo( listOf( Interaction( @@ -136,7 +136,7 @@ class AdvancedSettingsViewTest : RobolectricTest() { @Test fun `clicking on media to disable compression emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() val analyticsService = FakeAnalyticsService() setAdvancedSettingsView( state = aAdvancedSettingsState( @@ -146,7 +146,7 @@ class AdvancedSettingsViewTest : RobolectricTest() { analyticsService = analyticsService ) clickOn(R.string.screen_advanced_settings_media_compression_description) - eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(false)) + eventsRecorder.assertSingle(AdvancedSettingsEvent.SetCompressMedia(false)) assertThat(analyticsService.capturedEvents).isEqualTo( listOf( Interaction( @@ -159,7 +159,7 @@ class AdvancedSettingsViewTest : RobolectricTest() { @Test @Config(qualifiers = "h1080dp") fun `clicking on hide invite avatars emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, @@ -167,13 +167,13 @@ class AdvancedSettingsViewTest : RobolectricTest() { ), ) clickOn(R.string.screen_advanced_settings_hide_invite_avatars_toggle_title) - eventsRecorder.assertSingle(AdvancedSettingsEvents.SetHideInviteAvatars(true)) + eventsRecorder.assertSingle(AdvancedSettingsEvent.SetHideInviteAvatars(true)) } @Test @Config(qualifiers = "h1080dp") fun `clicking on timeline media preview always hide emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, @@ -181,13 +181,13 @@ class AdvancedSettingsViewTest : RobolectricTest() { ), ) clickOn(R.string.screen_advanced_settings_show_media_timeline_always_hide) - eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Off)) + eventsRecorder.assertSingle(AdvancedSettingsEvent.SetTimelineMediaPreviewValue(MediaPreviewValue.Off)) } @Test @Config(qualifiers = "h1080dp") fun `clicking on timeline media preview private rooms emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, @@ -195,13 +195,13 @@ class AdvancedSettingsViewTest : RobolectricTest() { ), ) clickOn(R.string.screen_advanced_settings_show_media_timeline_private_rooms) - eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.Private)) + eventsRecorder.assertSingle(AdvancedSettingsEvent.SetTimelineMediaPreviewValue(MediaPreviewValue.Private)) } @Test @Config(qualifiers = "h1080dp") fun `clicking on timeline media preview always show emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, @@ -209,13 +209,13 @@ class AdvancedSettingsViewTest : RobolectricTest() { ), ) clickOn(R.string.screen_advanced_settings_show_media_timeline_always_show) - eventsRecorder.assertSingle(AdvancedSettingsEvents.SetTimelineMediaPreviewValue(MediaPreviewValue.On)) + eventsRecorder.assertSingle(AdvancedSettingsEvent.SetTimelineMediaPreviewValue(MediaPreviewValue.On)) } @Test @Config(qualifiers = "h1080dp") fun `hide invite avatars toggle is disabled when action is loading`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, @@ -230,7 +230,7 @@ class AdvancedSettingsViewTest : RobolectricTest() { @Test @Config(qualifiers = "h1080dp") fun `timeline media preview options are disabled when action is loading`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) setAdvancedSettingsView( state = aAdvancedSettingsState( eventSink = eventsRecorder, diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUserViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUserViewTest.kt index 58e0cf467f..99ae35e70f 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUserViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUserViewTest.kt @@ -33,7 +33,7 @@ import org.junit.Test class BlockedUserViewTest : RobolectricTest() { @Test fun `clicking on back invokes back callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> setBlockedUsersView( aBlockedUsersState( @@ -47,7 +47,7 @@ class BlockedUserViewTest : RobolectricTest() { @Test fun `clicking on a user emits the expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() val userList = aMatrixUserList() setBlockedUsersView( aBlockedUsersState( @@ -56,12 +56,12 @@ class BlockedUserViewTest : RobolectricTest() { ), ) onNodeWithText(userList.first().displayName.orEmpty()).performClick() - eventsRecorder.assertSingle(BlockedUsersEvents.Unblock(userList.first().userId)) + eventsRecorder.assertSingle(BlockedUsersEvent.Unblock(userList.first().userId)) } @Test fun `long clicking on a user emits the expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() val userList = aMatrixUserList() setBlockedUsersView( aBlockedUsersState( @@ -70,12 +70,12 @@ class BlockedUserViewTest : RobolectricTest() { ), ) onNodeWithText(userList.first().displayName.orEmpty()).performTouchInput { longClick() } - eventsRecorder.assertSingle(BlockedUsersEvents.CopyToClipboard(userList.first().userId)) + eventsRecorder.assertSingle(BlockedUsersEvent.CopyToClipboard(userList.first().userId)) } @Test - fun `clicking on cancel sends a BlockedUsersEvents`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + fun `clicking on cancel sends a BlockedUsersEvent`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() setBlockedUsersView( aBlockedUsersState( unblockUserAction = AsyncAction.ConfirmingNoParams, @@ -83,12 +83,12 @@ class BlockedUserViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_cancel) - eventsRecorder.assertSingle(BlockedUsersEvents.Cancel) + eventsRecorder.assertSingle(BlockedUsersEvent.Cancel) } @Test - fun `clicking on confirm sends a BlockedUsersEvents`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + fun `clicking on confirm sends a BlockedUsersEvent`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() setBlockedUsersView( aBlockedUsersState( unblockUserAction = AsyncAction.ConfirmingNoParams, @@ -96,7 +96,7 @@ class BlockedUserViewTest : RobolectricTest() { ), ) clickOn(R.string.screen_blocked_users_unblock_alert_action) - eventsRecorder.assertSingle(BlockedUsersEvents.ConfirmUnblock) + eventsRecorder.assertSingle(BlockedUsersEvent.ConfirmUnblock) } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTest.kt index 4e0164b2a0..828a0fc59a 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/blockedusers/BlockedUsersPresenterTest.kt @@ -118,10 +118,10 @@ class BlockedUsersPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink(BlockedUsersEvents.Unblock(A_USER_ID)) + initialState.eventSink(BlockedUsersEvent.Unblock(A_USER_ID)) assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Confirming::class.java) - initialState.eventSink(BlockedUsersEvents.ConfirmUnblock) + initialState.eventSink(BlockedUsersEvent.ConfirmUnblock) assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Loading::class.java) assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Success::class.java) @@ -141,10 +141,10 @@ class BlockedUsersPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink(BlockedUsersEvents.Unblock(A_USER_ID)) + initialState.eventSink(BlockedUsersEvent.Unblock(A_USER_ID)) assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Confirming::class.java) - initialState.eventSink(BlockedUsersEvents.ConfirmUnblock) + initialState.eventSink(BlockedUsersEvent.ConfirmUnblock) assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Loading::class.java) assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Failure::class.java) @@ -163,10 +163,10 @@ class BlockedUsersPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink(BlockedUsersEvents.Unblock(A_USER_ID)) + initialState.eventSink(BlockedUsersEvent.Unblock(A_USER_ID)) assertThat(awaitItem().unblockUserAction).isInstanceOf(AsyncAction.Confirming::class.java) - initialState.eventSink(BlockedUsersEvents.Cancel) + initialState.eventSink(BlockedUsersEvent.Cancel) assertThat(awaitItem().unblockUserAction).isEqualTo(AsyncAction.Uninitialized) } @@ -187,7 +187,7 @@ class BlockedUsersPresenterTest { }.test { val initialState = awaitItem() assertThat(initialState.snackbarMessage).isNull() - initialState.eventSink(BlockedUsersEvents.CopyToClipboard(A_USER_ID)) + initialState.eventSink(BlockedUsersEvent.CopyToClipboard(A_USER_ID)) assertThat(clipboardHelper.clipboardContents).isEqualTo(A_USER_ID.value) assertThat(awaitItem().snackbarMessage?.messageResId).isEqualTo(CommonStrings.common_copied_to_clipboard) @@ -201,7 +201,7 @@ class BlockedUsersPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - awaitItem().eventSink(BlockedUsersEvents.ConfirmUnblock) + awaitItem().eventSink(BlockedUsersEvent.ConfirmUnblock) ensureAllEventsConsumed() } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index 3b2645783f..753d89da95 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -88,7 +88,7 @@ class DeveloperSettingsPresenterTest { skipItems(2) assertThat(clearCacheUseCase.executeHasBeenCalled).isFalse() awaitItem().also { state -> - state.eventSink(DeveloperSettingsEvents.ClearCache) + state.eventSink(DeveloperSettingsEvent.ClearCache) } awaitItem().also { state -> assertThat(state.clearCacheAction).isInstanceOf(AsyncAction.Loading::class.java) @@ -117,13 +117,13 @@ class DeveloperSettingsPresenterTest { skipItems(1) val initialState = awaitItem() assertThat(initialState.isEnterpriseBuild).isTrue() - initialState.eventSink(DeveloperSettingsEvents.SetShowColorPicker(true)) + initialState.eventSink(DeveloperSettingsEvent.SetShowColorPicker(true)) assertThat(awaitItem().showColorPicker).isTrue() - initialState.eventSink(DeveloperSettingsEvents.SetShowColorPicker(false)) + initialState.eventSink(DeveloperSettingsEvent.SetShowColorPicker(false)) assertThat(awaitItem().showColorPicker).isFalse() - initialState.eventSink(DeveloperSettingsEvents.SetShowColorPicker(true)) + initialState.eventSink(DeveloperSettingsEvent.SetShowColorPicker(true)) assertThat(awaitItem().showColorPicker).isTrue() - initialState.eventSink(DeveloperSettingsEvents.ChangeBrandColor(Color.Green)) + initialState.eventSink(DeveloperSettingsEvent.ChangeBrandColor(Color.Green)) assertThat(awaitItem().showColorPicker).isFalse() skipItems(1) overrideBrandColorResult.assertions().isCalledOnce() @@ -138,10 +138,10 @@ class DeveloperSettingsPresenterTest { presenter.test { skipItems(2) val initialState = awaitItem() - initialState.eventSink(DeveloperSettingsEvents.MarkAllRoomsAsRead(needsConfirmation = true)) + initialState.eventSink(DeveloperSettingsEvent.MarkAllRoomsAsRead(needsConfirmation = true)) val stateWithConfirmation = awaitItem() assertThat(stateWithConfirmation.markAllRoomsAsReadAction.isConfirming()).isTrue() - stateWithConfirmation.eventSink(DeveloperSettingsEvents.MarkAllRoomsAsRead(needsConfirmation = false)) + stateWithConfirmation.eventSink(DeveloperSettingsEvent.MarkAllRoomsAsRead(needsConfirmation = false)) awaitItem().also { state -> assertThat(state.markAllRoomsAsReadAction.isConfirming()).isFalse() assertThat(state.markAllRoomsAsReadAction).isInstanceOf(AsyncAction.Loading::class.java) @@ -164,7 +164,7 @@ class DeveloperSettingsPresenterTest { presenter.test { val state = awaitItem() assertThat(vacuumCalled).isFalse() - state.eventSink(DeveloperSettingsEvents.VacuumStores) + state.eventSink(DeveloperSettingsEvent.VacuumStores) skipItems(1) assertThat(vacuumCalled).isTrue() } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt index a16ad77d48..f1151bd3e1 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsViewTest.kt @@ -30,7 +30,7 @@ import org.robolectric.annotation.Config class DeveloperSettingsViewTest : RobolectricTest() { @Test fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { setDeveloperSettingsView( state = aDeveloperSettingsState( @@ -45,7 +45,7 @@ class DeveloperSettingsViewTest : RobolectricTest() { @Config(qualifiers = "h2400dp") @Test fun `clicking on push history notification invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { setDeveloperSettingsView( state = aDeveloperSettingsState( @@ -60,7 +60,7 @@ class DeveloperSettingsViewTest : RobolectricTest() { @Config(qualifiers = "h2000dp") @Test fun `clicking on open showkase invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { setDeveloperSettingsView( state = aDeveloperSettingsState( @@ -75,14 +75,14 @@ class DeveloperSettingsViewTest : RobolectricTest() { @Config(qualifiers = "h2400dp") @Test fun `clicking on clear cache emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setDeveloperSettingsView( state = aDeveloperSettingsState( eventSink = eventsRecorder ), ) onNodeWithText("Clear cache").performScrollTo().performClick() - eventsRecorder.assertSingle(DeveloperSettingsEvents.ClearCache) + eventsRecorder.assertSingle(DeveloperSettingsEvent.ClearCache) } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/labs/LabsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/labs/LabsPresenterTest.kt index acf65ef2e6..1173ce9c5f 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/labs/LabsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/labs/LabsPresenterTest.kt @@ -70,10 +70,10 @@ class LabsPresenterTest { val feature = initialItem.features.first() assertThat(feature.isEnabled).isFalse() // Toggle the feature, should be true now - initialItem.eventSink(LabsEvents.ToggleFeature(feature)) + initialItem.eventSink(LabsEvent.ToggleFeature(feature)) assertThat(awaitItem().features.first().isEnabled).isTrue() // Toggle the feature, should be false now - initialItem.eventSink(LabsEvents.ToggleFeature(feature)) + initialItem.eventSink(LabsEvent.ToggleFeature(feature)) assertThat(awaitItem().features.first().isEnabled).isFalse() } } @@ -99,7 +99,7 @@ class LabsPresenterTest { assertThat(feature.isEnabled).isFalse() assertThat(initialItem.isApplyingChanges).isFalse() // Toggle the feature - initialItem.eventSink(LabsEvents.ToggleFeature(feature)) + initialItem.eventSink(LabsEvent.ToggleFeature(feature)) assertThat(awaitItem().features.first().isEnabled).isTrue() // The clear cache use case should have been called assertThat(awaitItem().isApplyingChanges).isTrue() diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTest.kt index 509d00db25..10826071e7 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/EditDefaultNotificationSettingsPresenterTest.kt @@ -10,7 +10,7 @@ package io.element.android.features.preferences.impl.notifications import com.google.common.truth.Truth.assertThat import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingPresenter -import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingStateEvents +import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingStateEvent import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -150,7 +150,7 @@ class EditDefaultNotificationSettingsPresenterTest { ), ) presenter.test { - awaitItem().eventSink(EditDefaultNotificationSettingStateEvents.SetNotificationMode(RoomNotificationMode.ALL_MESSAGES)) + awaitItem().eventSink(EditDefaultNotificationSettingStateEvent.SetNotificationMode(RoomNotificationMode.ALL_MESSAGES)) val loadedState = consumeItemsUntilPredicate { it.mode == RoomNotificationMode.ALL_MESSAGES }.last() @@ -166,12 +166,12 @@ class EditDefaultNotificationSettingsPresenterTest { val presenter = createEditDefaultNotificationSettingPresenter(notificationSettingsService) notificationSettingsService.givenSetDefaultNotificationModeError(AN_EXCEPTION) presenter.test { - awaitItem().eventSink(EditDefaultNotificationSettingStateEvents.SetNotificationMode(RoomNotificationMode.ALL_MESSAGES)) + awaitItem().eventSink(EditDefaultNotificationSettingStateEvent.SetNotificationMode(RoomNotificationMode.ALL_MESSAGES)) val errorState = consumeItemsUntilPredicate { it.changeNotificationSettingAction.isFailure() }.last() assertThat(errorState.changeNotificationSettingAction.isFailure()).isTrue() - errorState.eventSink(EditDefaultNotificationSettingStateEvents.ClearError) + errorState.eventSink(EditDefaultNotificationSettingStateEvent.ClearError) val clearErrorState = consumeItemsUntilPredicate { it.changeNotificationSettingAction.isUninitialized() }.last() diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt index 29e4b1c40a..6a201fe3fa 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTest.kt @@ -120,7 +120,7 @@ class NotificationSettingsPresenterTest { val presenter = createNotificationSettingsPresenter(notificationSettingsService) presenter.test { val initialState = awaitItem() - initialState.eventSink(NotificationSettingsEvents.FixConfigurationMismatch) + initialState.eventSink(NotificationSettingsEvent.FixConfigurationMismatch) val fixedState = consumeItemsUntilPredicate(timeout = 2000.milliseconds) { it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid }.last() @@ -148,14 +148,14 @@ class NotificationSettingsPresenterTest { it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid }.last() assertThat(loadedState.appSettings.appNotificationsEnabled).isTrue() - loadedState.eventSink(NotificationSettingsEvents.SetNotificationsEnabled(false)) + loadedState.eventSink(NotificationSettingsEvent.SetNotificationsEnabled(false)) val updatedState = consumeItemsUntilPredicate { !it.appSettings.appNotificationsEnabled }.last() assertThat(updatedState.appSettings.appNotificationsEnabled).isFalse() unregisterWithResult.assertions().isCalledOnce() // Enable notification again - loadedState.eventSink(NotificationSettingsEvents.SetNotificationsEnabled(true)) + loadedState.eventSink(NotificationSettingsEvent.SetNotificationsEnabled(true)) val updatedState2 = consumeItemsUntilPredicate { it.appSettings.appNotificationsEnabled }.last() @@ -173,7 +173,7 @@ class NotificationSettingsPresenterTest { }.last() val validMatrixState = loadedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid assertThat(validMatrixState?.callNotificationsEnabled).isFalse() - loadedState.eventSink(NotificationSettingsEvents.SetCallNotificationsEnabled(true)) + loadedState.eventSink(NotificationSettingsEvent.SetCallNotificationsEnabled(true)) val updatedState = consumeItemsUntilPredicate { (it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid)?.callNotificationsEnabled == true }.last() @@ -192,7 +192,7 @@ class NotificationSettingsPresenterTest { }.last() val validMatrixState = loadedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid assertThat(validMatrixState?.inviteForMeNotificationsEnabled).isFalse() - loadedState.eventSink(NotificationSettingsEvents.SetInviteForMeNotificationsEnabled(true)) + loadedState.eventSink(NotificationSettingsEvent.SetInviteForMeNotificationsEnabled(true)) val updatedState = consumeItemsUntilPredicate { (it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid)?.inviteForMeNotificationsEnabled == true }.last() @@ -211,7 +211,7 @@ class NotificationSettingsPresenterTest { }.last() val validMatrixState = loadedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid assertThat(validMatrixState?.atRoomNotificationsEnabled).isFalse() - loadedState.eventSink(NotificationSettingsEvents.SetAtRoomNotificationsEnabled(true)) + loadedState.eventSink(NotificationSettingsEvent.SetAtRoomNotificationsEnabled(true)) val updatedState = consumeItemsUntilPredicate { (it.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid)?.atRoomNotificationsEnabled == true }.last() @@ -232,12 +232,12 @@ class NotificationSettingsPresenterTest { }.last() val validMatrixState = loadedState.matrixSettings as? NotificationSettingsState.MatrixSettings.Valid assertThat(validMatrixState?.atRoomNotificationsEnabled).isFalse() - loadedState.eventSink(NotificationSettingsEvents.SetAtRoomNotificationsEnabled(true)) + loadedState.eventSink(NotificationSettingsEvent.SetAtRoomNotificationsEnabled(true)) val errorState = consumeItemsUntilPredicate { it.changeNotificationSettingAction.isFailure() }.last() assertThat(errorState.changeNotificationSettingAction.isFailure()).isTrue() - errorState.eventSink(NotificationSettingsEvents.ClearNotificationChangeError) + errorState.eventSink(NotificationSettingsEvent.ClearNotificationChangeError) val clearErrorState = consumeItemsUntilPredicate { it.changeNotificationSettingAction.isUninitialized() }.last() @@ -258,16 +258,16 @@ class NotificationSettingsPresenterTest { Distributor(value = "aDistributorValue0", name = "aDistributorName0"), Distributor(value = "aDistributorValue1", name = "aDistributorName1"), ) - initialState.eventSink.invoke(NotificationSettingsEvents.ChangePushProvider) + initialState.eventSink.invoke(NotificationSettingsEvent.ChangePushProvider) val withDialog = awaitItem() assertThat(withDialog.showChangePushProviderDialog).isTrue() // Cancel - withDialog.eventSink(NotificationSettingsEvents.CancelChangePushProvider) + withDialog.eventSink(NotificationSettingsEvent.CancelChangePushProvider) val withoutDialog = awaitItem() assertThat(withoutDialog.showChangePushProviderDialog).isFalse() - withDialog.eventSink.invoke(NotificationSettingsEvents.ChangePushProvider) + withDialog.eventSink.invoke(NotificationSettingsEvent.ChangePushProvider) assertThat(awaitItem().showChangePushProviderDialog).isTrue() - withDialog.eventSink(NotificationSettingsEvents.SetPushProvider(1)) + withDialog.eventSink(NotificationSettingsEvent.SetPushProvider(1)) val withNewProvider = awaitItem() assertThat(withNewProvider.showChangePushProviderDialog).isFalse() assertThat(withNewProvider.currentPushDistributor).isInstanceOf(AsyncData.Loading::class.java) @@ -290,10 +290,10 @@ class NotificationSettingsPresenterTest { Distributor(value = "aDistributorValue0", name = "aDistributorName0"), Distributor(value = "aDistributorValue1", name = "aDistributorName1"), ) - initialState.eventSink.invoke(NotificationSettingsEvents.ChangePushProvider) + initialState.eventSink.invoke(NotificationSettingsEvent.ChangePushProvider) assertThat(awaitItem().showChangePushProviderDialog).isTrue() // Choose the same value (index 0) - initialState.eventSink(NotificationSettingsEvents.SetPushProvider(0)) + initialState.eventSink(NotificationSettingsEvent.SetPushProvider(0)) val withNewProvider = awaitItem() assertThat(withNewProvider.showChangePushProviderDialog).isFalse() expectNoEvents() @@ -318,7 +318,7 @@ class NotificationSettingsPresenterTest { expectNoEvents() // Refresh - initialState.eventSink.invoke(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) + initialState.eventSink.invoke(NotificationSettingsEvent.RefreshSystemNotificationsEnabled) assertThat(awaitItem().fullScreenIntentPermissionsState.permissionGranted).isTrue() } } @@ -334,10 +334,10 @@ class NotificationSettingsPresenterTest { ) presenter.test { val initialState = awaitLastSequentialItem() - initialState.eventSink.invoke(NotificationSettingsEvents.ChangePushProvider) + initialState.eventSink.invoke(NotificationSettingsEvent.ChangePushProvider) val withDialog = awaitItem() assertThat(withDialog.showChangePushProviderDialog).isTrue() - withDialog.eventSink(NotificationSettingsEvents.SetPushProvider(1)) + withDialog.eventSink(NotificationSettingsEvent.SetPushProvider(1)) val withNewProvider = awaitItem() assertThat(withNewProvider.showChangePushProviderDialog).isFalse() assertThat(withNewProvider.currentPushDistributor).isInstanceOf(AsyncData.Loading::class.java) @@ -384,7 +384,7 @@ class NotificationSettingsPresenterTest { }.last() assertThat(initialState.messageSound.sound).isEqualTo(NotificationSound.SystemDefault) - initialState.eventSink(NotificationSettingsEvents.SetMessageSound(customSound)) + initialState.eventSink(NotificationSettingsEvent.SetMessageSound(customSound)) val updatedState = consumeItemsUntilPredicate { it.messageSound.sound == customSound }.last() assertThat(updatedState.messageSound.sound).isEqualTo(customSound) @@ -410,7 +410,7 @@ class NotificationSettingsPresenterTest { }.last() assertThat(initialState.callRingtone.sound).isEqualTo(NotificationSound.SystemDefault) - initialState.eventSink(NotificationSettingsEvents.SetCallRingtone(NotificationSound.Silent)) + initialState.eventSink(NotificationSettingsEvent.SetCallRingtone(NotificationSound.Silent)) val updatedState = consumeItemsUntilPredicate { it.callRingtone.sound == NotificationSound.Silent }.last() assertThat(updatedState.callRingtone.sound).isEqualTo(NotificationSound.Silent) @@ -435,11 +435,11 @@ class NotificationSettingsPresenterTest { it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid }.last() - initialState.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.Custom("content://a"))) + initialState.eventSink(NotificationSettingsEvent.SetMessageSound(NotificationSound.Custom("content://a"))) consumeItemsUntilPredicate { it.messageSound.sound == NotificationSound.Custom("content://a") } - initialState.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.Custom("content://b"))) + initialState.eventSink(NotificationSettingsEvent.SetMessageSound(NotificationSound.Custom("content://b"))) consumeItemsUntilPredicate { it.messageSound.sound == NotificationSound.Custom("content://b") } - initialState.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.SystemDefault)) + initialState.eventSink(NotificationSettingsEvent.SetMessageSound(NotificationSound.SystemDefault)) consumeItemsUntilPredicate { it.messageSound.sound == NotificationSound.SystemDefault } assertThat(versions).containsExactly(1, 2, 3).inOrder() @@ -461,7 +461,7 @@ class NotificationSettingsPresenterTest { consumeItemsUntilPredicate { it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid && it.messageSound.sound == NotificationSound.Custom("content://existing") - }.last().eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.SystemDefault)) + }.last().eventSink(NotificationSettingsEvent.SetMessageSound(NotificationSound.SystemDefault)) consumeItemsUntilPredicate { it.messageSound.sound == NotificationSound.SystemDefault } assertThat(appPreferencesStore.getNotificationSoundChannelConfig().messageSound).isEqualTo(NotificationSound.SystemDefault) recreateRecorder.assertions().isCalledOnce().with(value(NotificationSound.SystemDefault), value(1)) @@ -483,7 +483,7 @@ class NotificationSettingsPresenterTest { it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid }.last() - initialState.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.Custom("content://x"))) + initialState.eventSink(NotificationSettingsEvent.SetMessageSound(NotificationSound.Custom("content://x"))) val errorState = consumeItemsUntilPredicate { it.changeNotificationSettingAction.isFailure() @@ -670,7 +670,7 @@ class NotificationSettingsPresenterTest { val initialState = consumeItemsUntilPredicate { it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid }.last() - initialState.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.Custom("content://media/source"))) + initialState.eventSink(NotificationSettingsEvent.SetMessageSound(NotificationSound.Custom("content://media/source"))) advanceUntilIdle() val expectedSound = NotificationSound.Custom("content://my.app.fileprovider/notification_sounds/message_sound.ogg") @@ -695,7 +695,7 @@ class NotificationSettingsPresenterTest { val initialState = consumeItemsUntilPredicate { it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid }.last() - initialState.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.Custom("content://broken"))) + initialState.eventSink(NotificationSettingsEvent.SetMessageSound(NotificationSound.Custom("content://broken"))) val errorState = consumeItemsUntilPredicate { it.messageSound.copyError }.last() assertThat(errorState.messageSound.copyError).isTrue() @@ -703,7 +703,7 @@ class NotificationSettingsPresenterTest { assertThat(appPreferencesStore.getNotificationSoundChannelConfig().messageSound).isEqualTo(priorChoice) // Dismissing the inline error clears the flag without altering persistence. - errorState.eventSink(NotificationSettingsEvents.DismissMessageSoundCopyError) + errorState.eventSink(NotificationSettingsEvent.DismissMessageSoundCopyError) val cleared = consumeItemsUntilPredicate { !it.messageSound.copyError }.last() assertThat(cleared.messageSound.copyError).isFalse() assertThat(appPreferencesStore.getNotificationSoundChannelConfig().messageSound).isEqualTo(priorChoice) @@ -725,7 +725,7 @@ class NotificationSettingsPresenterTest { val initialState = consumeItemsUntilPredicate { it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid }.last() - initialState.eventSink(NotificationSettingsEvents.SetCallRingtone(NotificationSound.Custom("content://broken"))) + initialState.eventSink(NotificationSettingsEvent.SetCallRingtone(NotificationSound.Custom("content://broken"))) val errorState = consumeItemsUntilPredicate { it.callRingtone.copyError }.last() assertThat(errorState.callRingtone.copyError).isTrue() @@ -733,7 +733,7 @@ class NotificationSettingsPresenterTest { assertThat(appPreferencesStore.getNotificationSoundChannelConfig().callRingtone).isEqualTo(priorChoice) // Dismissing the inline error clears the flag without altering persistence. - errorState.eventSink(NotificationSettingsEvents.DismissCallRingtoneCopyError) + errorState.eventSink(NotificationSettingsEvent.DismissCallRingtoneCopyError) val cleared = consumeItemsUntilPredicate { !it.callRingtone.copyError }.last() assertThat(cleared.callRingtone.copyError).isFalse() assertThat(appPreferencesStore.getNotificationSoundChannelConfig().callRingtone).isEqualTo(priorChoice) @@ -759,8 +759,8 @@ class NotificationSettingsPresenterTest { val initialState = consumeItemsUntilPredicate { it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid }.last() - initialState.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.SystemDefault)) - initialState.eventSink(NotificationSettingsEvents.SetCallRingtone(NotificationSound.Silent)) + initialState.eventSink(NotificationSettingsEvent.SetMessageSound(NotificationSound.SystemDefault)) + initialState.eventSink(NotificationSettingsEvent.SetCallRingtone(NotificationSound.Silent)) advanceUntilIdle() assertThat(copierCalls).isEmpty() cancelAndIgnoreRemainingEvents() @@ -791,8 +791,8 @@ class NotificationSettingsPresenterTest { val initialState = consumeItemsUntilPredicate { it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid }.last() - initialState.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.SystemDefault)) - initialState.eventSink(NotificationSettingsEvents.SetCallRingtone(NotificationSound.Silent)) + initialState.eventSink(NotificationSettingsEvent.SetMessageSound(NotificationSound.SystemDefault)) + initialState.eventSink(NotificationSettingsEvent.SetCallRingtone(NotificationSound.Silent)) advanceUntilIdle() assertThat(deleteCalls).containsExactly( NotificationSoundCopier.SoundSlot.Message, @@ -835,8 +835,8 @@ class NotificationSettingsPresenterTest { // Re-pick the persisted sound for both slots — neither should churn the channel, // bump the version, or hit the copier / delete sweep. - initial.eventSink(NotificationSettingsEvents.SelectMessageSoundPreset(NotificationSound.ElementDefault)) - initial.eventSink(NotificationSettingsEvents.SetCallRingtone(NotificationSound.Silent)) + initial.eventSink(NotificationSettingsEvent.SelectMessageSoundPreset(NotificationSound.ElementDefault)) + initial.eventSink(NotificationSettingsEvent.SetCallRingtone(NotificationSound.Silent)) advanceUntilIdle() val config = appPreferencesStore.getNotificationSoundChannelConfig() @@ -873,8 +873,8 @@ class NotificationSettingsPresenterTest { val initial = consumeItemsUntilPredicate { it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid }.last() - initial.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.Custom("content://A"))) - initial.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.Custom("content://B"))) + initial.eventSink(NotificationSettingsEvent.SetMessageSound(NotificationSound.Custom("content://A"))) + initial.eventSink(NotificationSettingsEvent.SetMessageSound(NotificationSound.Custom("content://B"))) advanceUntilIdle() assertThat(copyOrder).containsExactly("content://A", "content://B").inOrder() @@ -908,7 +908,7 @@ class NotificationSettingsPresenterTest { val initialState = consumeItemsUntilPredicate { it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid }.last() - initialState.eventSink(NotificationSettingsEvents.SetMessageSound(NotificationSound.Custom("content://media/source"))) + initialState.eventSink(NotificationSettingsEvent.SetMessageSound(NotificationSound.Custom("content://media/source"))) advanceUntilIdle() assertThat(deleteCalls).isEmpty() cancelAndIgnoreRemainingEvents() @@ -924,11 +924,11 @@ class NotificationSettingsPresenterTest { }.last() assertThat(initial.showMessageSoundDialog).isFalse() - initial.eventSink(NotificationSettingsEvents.ShowMessageSoundDialog) + initial.eventSink(NotificationSettingsEvent.ShowMessageSoundDialog) val shown = consumeItemsUntilPredicate { it.showMessageSoundDialog }.last() assertThat(shown.showMessageSoundDialog).isTrue() - shown.eventSink(NotificationSettingsEvents.DismissMessageSoundDialog) + shown.eventSink(NotificationSettingsEvent.DismissMessageSoundDialog) val hidden = consumeItemsUntilPredicate { !it.showMessageSoundDialog }.last() assertThat(hidden.showMessageSoundDialog).isFalse() cancelAndIgnoreRemainingEvents() @@ -950,10 +950,10 @@ class NotificationSettingsPresenterTest { val initial = consumeItemsUntilPredicate { it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid }.last() - initial.eventSink(NotificationSettingsEvents.ShowMessageSoundDialog) + initial.eventSink(NotificationSettingsEvent.ShowMessageSoundDialog) val shown = consumeItemsUntilPredicate { it.showMessageSoundDialog }.last() - shown.eventSink(NotificationSettingsEvents.SelectMessageSoundPreset(NotificationSound.ElementDefault)) + shown.eventSink(NotificationSettingsEvent.SelectMessageSoundPreset(NotificationSound.ElementDefault)) advanceUntilIdle() consumeItemsUntilPredicate { !it.showMessageSoundDialog && it.messageSound.sound == NotificationSound.ElementDefault @@ -973,10 +973,10 @@ class NotificationSettingsPresenterTest { it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid }.last() assertThat(initial.pendingMessageSoundPickerLaunch).isEqualTo(0) - initial.eventSink(NotificationSettingsEvents.ShowMessageSoundDialog) + initial.eventSink(NotificationSettingsEvent.ShowMessageSoundDialog) val shown = consumeItemsUntilPredicate { it.showMessageSoundDialog }.last() - shown.eventSink(NotificationSettingsEvents.LaunchMessageSoundPicker) + shown.eventSink(NotificationSettingsEvent.LaunchMessageSoundPicker) val launched = consumeItemsUntilPredicate { it.pendingMessageSoundPickerLaunch == 1 }.last() assertThat(launched.showMessageSoundDialog).isFalse() assertThat(launched.pendingMessageSoundPickerLaunch).isEqualTo(1) @@ -993,11 +993,11 @@ class NotificationSettingsPresenterTest { }.last() assertThat(initial.showCallRingtoneDialog).isFalse() - initial.eventSink(NotificationSettingsEvents.ShowCallRingtoneDialog) + initial.eventSink(NotificationSettingsEvent.ShowCallRingtoneDialog) val shown = consumeItemsUntilPredicate { it.showCallRingtoneDialog }.last() assertThat(shown.showCallRingtoneDialog).isTrue() - shown.eventSink(NotificationSettingsEvents.DismissCallRingtoneDialog) + shown.eventSink(NotificationSettingsEvent.DismissCallRingtoneDialog) val hidden = consumeItemsUntilPredicate { !it.showCallRingtoneDialog }.last() assertThat(hidden.showCallRingtoneDialog).isFalse() cancelAndIgnoreRemainingEvents() @@ -1020,10 +1020,10 @@ class NotificationSettingsPresenterTest { it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid && it.callRingtone.sound == NotificationSound.Silent }.last() - initial.eventSink(NotificationSettingsEvents.ShowCallRingtoneDialog) + initial.eventSink(NotificationSettingsEvent.ShowCallRingtoneDialog) val shown = consumeItemsUntilPredicate { it.showCallRingtoneDialog }.last() - shown.eventSink(NotificationSettingsEvents.SelectCallRingtonePreset(NotificationSound.SystemDefault)) + shown.eventSink(NotificationSettingsEvent.SelectCallRingtonePreset(NotificationSound.SystemDefault)) advanceUntilIdle() consumeItemsUntilPredicate { !it.showCallRingtoneDialog && it.callRingtone.sound == NotificationSound.SystemDefault @@ -1043,10 +1043,10 @@ class NotificationSettingsPresenterTest { it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid }.last() assertThat(initial.pendingCallRingtonePickerLaunch).isEqualTo(0) - initial.eventSink(NotificationSettingsEvents.ShowCallRingtoneDialog) + initial.eventSink(NotificationSettingsEvent.ShowCallRingtoneDialog) val shown = consumeItemsUntilPredicate { it.showCallRingtoneDialog }.last() - shown.eventSink(NotificationSettingsEvents.LaunchCallRingtonePicker) + shown.eventSink(NotificationSettingsEvent.LaunchCallRingtonePicker) val launched = consumeItemsUntilPredicate { it.pendingCallRingtonePickerLaunch == 1 }.last() assertThat(launched.showCallRingtoneDialog).isFalse() assertThat(launched.pendingCallRingtonePickerLaunch).isEqualTo(1) @@ -1078,7 +1078,7 @@ class NotificationSettingsPresenterTest { it.matrixSettings is NotificationSettingsState.MatrixSettings.Valid && it.callRingtone.sound == NotificationSound.Silent }.last() - initial.eventSink(NotificationSettingsEvents.SelectCallRingtonePreset(NotificationSound.SystemDefault)) + initial.eventSink(NotificationSettingsEvent.SelectCallRingtonePreset(NotificationSound.SystemDefault)) advanceUntilIdle() val after = consumeItemsUntilPredicate { it.callRingtone.sound == NotificationSound.SystemDefault diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt index 57edf01a1d..a28f43638f 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsViewTest.kt @@ -35,7 +35,7 @@ import org.robolectric.annotation.Config class NotificationSettingsViewTest : RobolectricTest() { @Test fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() ensureCalledOnce { setNotificationSettingsView( state = aValidNotificationSettingsState( @@ -45,13 +45,13 @@ class NotificationSettingsViewTest : RobolectricTest() { ) pressBack() } - eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) + eventsRecorder.assertSingle(NotificationSettingsEvent.RefreshSystemNotificationsEnabled) } @Config(qualifiers = "h1280dp") @Test fun `clicking on troubleshoot notification invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() ensureCalledOnce { setNotificationSettingsView( state = aValidNotificationSettingsState( @@ -61,13 +61,13 @@ class NotificationSettingsViewTest : RobolectricTest() { ) clickOn(R.string.troubleshoot_notifications_entry_point_title) } - eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) + eventsRecorder.assertSingle(NotificationSettingsEvent.RefreshSystemNotificationsEnabled) } @Config(qualifiers = "h1024dp") @Test fun `clicking on group chats invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() ensureCalledOnceWithParam(false) { setNotificationSettingsView( state = aValidNotificationSettingsState( @@ -77,13 +77,13 @@ class NotificationSettingsViewTest : RobolectricTest() { ) clickOn(R.string.screen_notification_settings_group_chats) } - eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) + eventsRecorder.assertSingle(NotificationSettingsEvent.RefreshSystemNotificationsEnabled) } @Config(qualifiers = "h1024dp") @Test fun `clicking on direct chats invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() ensureCalledOnceWithParam(true) { setNotificationSettingsView( state = aValidNotificationSettingsState( @@ -93,7 +93,7 @@ class NotificationSettingsViewTest : RobolectricTest() { ) clickOn(R.string.screen_notification_settings_direct_chats) } - eventsRecorder.assertSingle(NotificationSettingsEvents.RefreshSystemNotificationsEnabled) + eventsRecorder.assertSingle(NotificationSettingsEvent.RefreshSystemNotificationsEnabled) } @Config(qualifiers = "h1024dp") @@ -109,7 +109,7 @@ class NotificationSettingsViewTest : RobolectricTest() { } private fun testNotificationToggle(initialState: Boolean) = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setNotificationSettingsView( state = aValidNotificationSettingsState( appNotificationEnabled = initialState, @@ -119,8 +119,8 @@ class NotificationSettingsViewTest : RobolectricTest() { clickOn(R.string.screen_notification_settings_enable_notifications) eventsRecorder.assertList( listOf( - NotificationSettingsEvents.RefreshSystemNotificationsEnabled, - NotificationSettingsEvents.SetNotificationsEnabled(!initialState) + NotificationSettingsEvent.RefreshSystemNotificationsEnabled, + NotificationSettingsEvent.SetNotificationsEnabled(!initialState) ) ) } @@ -138,7 +138,7 @@ class NotificationSettingsViewTest : RobolectricTest() { } private fun testAtRoomToggle(initialState: Boolean) = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setNotificationSettingsView( state = aValidNotificationSettingsState( atRoomNotificationsEnabled = initialState, @@ -148,8 +148,8 @@ class NotificationSettingsViewTest : RobolectricTest() { clickOn(R.string.screen_notification_settings_room_mention_label) eventsRecorder.assertList( listOf( - NotificationSettingsEvents.RefreshSystemNotificationsEnabled, - NotificationSettingsEvents.SetAtRoomNotificationsEnabled(!initialState) + NotificationSettingsEvent.RefreshSystemNotificationsEnabled, + NotificationSettingsEvent.SetAtRoomNotificationsEnabled(!initialState) ) ) } @@ -167,7 +167,7 @@ class NotificationSettingsViewTest : RobolectricTest() { } private fun testInvitationToggle(initialState: Boolean) = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setNotificationSettingsView( state = aValidNotificationSettingsState( inviteForMeNotificationsEnabled = initialState, @@ -177,8 +177,8 @@ class NotificationSettingsViewTest : RobolectricTest() { clickOn(R.string.screen_notification_settings_invite_for_me_label) eventsRecorder.assertList( listOf( - NotificationSettingsEvents.RefreshSystemNotificationsEnabled, - NotificationSettingsEvents.SetInviteForMeNotificationsEnabled(!initialState) + NotificationSettingsEvent.RefreshSystemNotificationsEnabled, + NotificationSettingsEvent.SetInviteForMeNotificationsEnabled(!initialState) ) ) } @@ -186,7 +186,7 @@ class NotificationSettingsViewTest : RobolectricTest() { @Config(qualifiers = "h1024dp") @Test fun `with an error configuration, clicking on continue emits the expected events`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setNotificationSettingsView( state = aValidNotificationSettingsState( changeNotificationSettingAction = AsyncAction.Failure(AN_EXCEPTION), @@ -196,8 +196,8 @@ class NotificationSettingsViewTest : RobolectricTest() { clickOn(CommonStrings.action_ok) eventsRecorder.assertList( listOf( - NotificationSettingsEvents.RefreshSystemNotificationsEnabled, - NotificationSettingsEvents.ClearNotificationChangeError + NotificationSettingsEvent.RefreshSystemNotificationsEnabled, + NotificationSettingsEvent.ClearNotificationChangeError ) ) } @@ -205,7 +205,7 @@ class NotificationSettingsViewTest : RobolectricTest() { @Config(qualifiers = "h1024dp") @Test fun `with invalid configuration, clicking on continue emits the expected events`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setNotificationSettingsView( state = aInvalidNotificationSettingsState( fixFailed = false, @@ -215,8 +215,8 @@ class NotificationSettingsViewTest : RobolectricTest() { clickOn(CommonStrings.action_continue) eventsRecorder.assertList( listOf( - NotificationSettingsEvents.RefreshSystemNotificationsEnabled, - NotificationSettingsEvents.FixConfigurationMismatch + NotificationSettingsEvent.RefreshSystemNotificationsEnabled, + NotificationSettingsEvent.FixConfigurationMismatch ) ) } @@ -224,7 +224,7 @@ class NotificationSettingsViewTest : RobolectricTest() { @Config(qualifiers = "h1024dp") @Test fun `with invalid configuration and error, clicking on OK emits the expected events`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setNotificationSettingsView( state = aInvalidNotificationSettingsState( fixFailed = true, @@ -234,8 +234,8 @@ class NotificationSettingsViewTest : RobolectricTest() { clickOn(CommonStrings.action_ok) eventsRecorder.assertList( listOf( - NotificationSettingsEvents.RefreshSystemNotificationsEnabled, - NotificationSettingsEvents.ClearConfigurationMismatchError + NotificationSettingsEvent.RefreshSystemNotificationsEnabled, + NotificationSettingsEvent.ClearConfigurationMismatchError ) ) } @@ -243,7 +243,7 @@ class NotificationSettingsViewTest : RobolectricTest() { @Config(qualifiers = "h1280dp") @Test fun `clicking on Push notification provider emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder @@ -252,15 +252,15 @@ class NotificationSettingsViewTest : RobolectricTest() { clickOn(R.string.screen_advanced_settings_push_provider_android) eventsRecorder.assertList( listOf( - NotificationSettingsEvents.RefreshSystemNotificationsEnabled, - NotificationSettingsEvents.ChangePushProvider, + NotificationSettingsEvent.RefreshSystemNotificationsEnabled, + NotificationSettingsEvent.ChangePushProvider, ) ) } @Test fun `clicking on a push provider emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder, @@ -271,8 +271,8 @@ class NotificationSettingsViewTest : RobolectricTest() { onNodeWithText("P2").performClick() eventsRecorder.assertList( listOf( - NotificationSettingsEvents.RefreshSystemNotificationsEnabled, - NotificationSettingsEvents.SetPushProvider(1), + NotificationSettingsEvent.RefreshSystemNotificationsEnabled, + NotificationSettingsEvent.SetPushProvider(1), ) ) } @@ -280,7 +280,7 @@ class NotificationSettingsViewTest : RobolectricTest() { @Config(qualifiers = "h1280dp") @Test fun `sounds preference category renders rows with current display names`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setNotificationSettingsView( state = aValidNotificationSettingsState( eventSink = eventsRecorder, @@ -298,7 +298,7 @@ class NotificationSettingsViewTest : RobolectricTest() { @Config(qualifiers = "h1280dp") @Test fun `clicking the message sound row opens the preset dialog`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setNotificationSettingsView( state = aValidNotificationSettingsState(eventSink = eventsRecorder), ) @@ -308,8 +308,8 @@ class NotificationSettingsViewTest : RobolectricTest() { onNodeWithText("Message sound").performClick() eventsRecorder.assertList( listOf( - NotificationSettingsEvents.RefreshSystemNotificationsEnabled, - NotificationSettingsEvents.ShowMessageSoundDialog, + NotificationSettingsEvent.RefreshSystemNotificationsEnabled, + NotificationSettingsEvent.ShowMessageSoundDialog, ) ) } diff --git a/features/preferences/test/build.gradle.kts b/features/preferences/test/build.gradle.kts index a066fe4707..5b8969796a 100644 --- a/features/preferences/test/build.gradle.kts +++ b/features/preferences/test/build.gradle.kts @@ -16,5 +16,7 @@ android { dependencies { implementation(projects.libraries.architecture) implementation(projects.features.preferences.api) + implementation(projects.libraries.matrix.api) + implementation(libs.coroutines.core) implementation(projects.tests.testutils) } diff --git a/features/preferences/test/src/main/kotlin/io/element/android/features/preferences/test/FakeCacheService.kt b/features/preferences/test/src/main/kotlin/io/element/android/features/preferences/test/FakeCacheService.kt new file mode 100644 index 0000000000..9696a2050b --- /dev/null +++ b/features/preferences/test/src/main/kotlin/io/element/android/features/preferences/test/FakeCacheService.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.preferences.test + +import io.element.android.features.preferences.api.CacheService +import io.element.android.libraries.matrix.api.core.SessionId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow + +class FakeCacheService( + override val clearedCacheEventFlow: Flow = emptyFlow(), +) : CacheService diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesStateProvider.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesStatePreviewParam.kt similarity index 91% rename from features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesStateProvider.kt rename to features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesStatePreviewParam.kt index 1de65c1a69..28ecc9f187 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesStateProvider.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesStatePreviewParam.kt @@ -10,7 +10,7 @@ package io.element.android.features.rageshake.api.preferences import androidx.compose.ui.tooling.preview.PreviewParameterProvider -open class RageshakePreferencesStateProvider : PreviewParameterProvider { +open class RageshakePreferencesStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRageshakePreferencesState(isEnabled = true, isSupported = true, sensitivity = 0.5f), diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesView.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesView.kt index a227d0ffcf..7b01f62b82 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesView.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/preferences/RageshakePreferencesView.kt @@ -68,6 +68,8 @@ fun RageshakePreferencesView( @PreviewsDayNight @Composable -internal fun RageshakePreferencesViewPreview(@PreviewParameter(RageshakePreferencesStateProvider::class) state: RageshakePreferencesState) = ElementPreview { +internal fun RageshakePreferencesViewPreview(@PreviewParameter( + RageshakePreferencesStatePreviewParam::class +) state: RageshakePreferencesState) = ElementPreview { RageshakePreferencesView(state) } diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportEvents.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportEvent.kt similarity index 74% rename from features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportEvents.kt rename to features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportEvent.kt index 94897d1bad..448f5a9d87 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportEvents.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportEvent.kt @@ -8,15 +8,15 @@ package io.element.android.features.rageshake.impl.bugreport -sealed interface BugReportEvents { - data object SendBugReport : BugReportEvents - data object ResetAll : BugReportEvents - data object ClearError : BugReportEvents +sealed interface BugReportEvent { + data object SendBugReport : BugReportEvent + data object ResetAll : BugReportEvent + data object ClearError : BugReportEvent - data class SetDescription(val description: String) : BugReportEvents - data class SetSendLog(val sendLog: Boolean) : BugReportEvents - data class SetCanContact(val canContact: Boolean) : BugReportEvents - data class SetSendScreenshot(val sendScreenshot: Boolean) : BugReportEvents - data class SetSendPushRules(val sendPushRules: Boolean) : BugReportEvents - data class SetGhIssueNumber(val ghIssueNumber: Int?) : BugReportEvents + data class SetDescription(val description: String) : BugReportEvent + data class SetSendLog(val sendLog: Boolean) : BugReportEvent + data class SetCanContact(val canContact: Boolean) : BugReportEvent + data class SetSendScreenshot(val sendScreenshot: Boolean) : BugReportEvent + data class SetSendPushRules(val sendPushRules: Boolean) : BugReportEvent + data class SetGhIssueNumber(val ghIssueNumber: Int?) : BugReportEvent } diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt index 85731ae959..607653baa4 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenter.kt @@ -83,37 +83,37 @@ class BugReportPresenter( } val uploadListener = BugReporterUploadListener(sendingProgress, sendingAction) - fun handleEvent(event: BugReportEvents) { + fun handleEvent(event: BugReportEvent) { when (event) { - BugReportEvents.SendBugReport -> { - if (formState.value.description.length < 10) { + BugReportEvent.SendBugReport -> { + if (formState.value.description.length < 10 && formState.value.ghIssueNumber == null) { sendingAction.value = AsyncAction.Failure(BugReportFormError.DescriptionTooShort) } else { sendingAction.value = AsyncAction.Loading appCoroutineScope.sendBugReport(formState.value, crashInfo.isNotEmpty(), uploadListener) } } - BugReportEvents.ResetAll -> appCoroutineScope.resetAll() - is BugReportEvents.SetDescription -> updateFormState(formState) { + BugReportEvent.ResetAll -> appCoroutineScope.resetAll() + is BugReportEvent.SetDescription -> updateFormState(formState) { copy(description = event.description) } - is BugReportEvents.SetCanContact -> updateFormState(formState) { + is BugReportEvent.SetCanContact -> updateFormState(formState) { copy(canContact = event.canContact) } - is BugReportEvents.SetSendLog -> updateFormState(formState) { + is BugReportEvent.SetSendLog -> updateFormState(formState) { copy(sendLogs = event.sendLog) } - is BugReportEvents.SetSendScreenshot -> updateFormState(formState) { + is BugReportEvent.SetSendScreenshot -> updateFormState(formState) { copy(sendScreenshot = event.sendScreenshot) } - is BugReportEvents.SetSendPushRules -> updateFormState(formState) { + is BugReportEvent.SetSendPushRules -> updateFormState(formState) { copy(sendPushRules = event.sendPushRules) } - BugReportEvents.ClearError -> { + BugReportEvent.ClearError -> { sendingProgress.floatValue = 0f sendingAction.value = AsyncAction.Uninitialized } - is BugReportEvents.SetGhIssueNumber -> updateFormState(formState) { + is BugReportEvent.SetGhIssueNumber -> updateFormState(formState) { copy(ghIssueNumber = event.ghIssueNumber) } } diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportState.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportState.kt index 8e07b3c4e8..4ec5710cc6 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportState.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportState.kt @@ -18,7 +18,7 @@ data class BugReportState( val screenshotUri: String?, val sendingProgress: Float, val sending: AsyncAction, - val eventSink: (BugReportEvents) -> Unit + val eventSink: (BugReportEvent) -> Unit ) { val submitEnabled = sending !is AsyncAction.Loading val isDescriptionInError = sending is AsyncAction.Failure && diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportStateProvider.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportStatePreviewParam.kt similarity index 94% rename from features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportStateProvider.kt rename to features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportStatePreviewParam.kt index 0afd06f22a..1e08c055b4 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportStateProvider.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportStatePreviewParam.kt @@ -11,7 +11,7 @@ package io.element.android.features.rageshake.impl.bugreport import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction -open class BugReportStateProvider : PreviewParameterProvider { +open class BugReportStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aBugReportState(), diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt index fede373627..9c1c25e153 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt @@ -84,7 +84,7 @@ fun BugReportView( supportingText = stringResource(id = R.string.screen_bug_report_editor_description), onValueChange = { descriptionFieldState = it - eventSink(BugReportEvents.SetDescription(it)) + eventSink(BugReportEvent.SetDescription(it)) }, keyboardOptions = KeyboardOptions( capitalization = KeyboardCapitalization.Sentences, @@ -110,14 +110,14 @@ fun BugReportView( PreferenceDivider() PreferenceSwitch( isChecked = state.formState.sendLogs, - onCheckedChange = { eventSink(BugReportEvents.SetSendLog(it)) }, + onCheckedChange = { eventSink(BugReportEvent.SetSendLog(it)) }, enabled = isFormEnabled, title = stringResource(id = R.string.screen_bug_report_include_logs), subtitle = stringResource(id = R.string.screen_bug_report_logs_description), ) PreferenceSwitch( isChecked = state.formState.canContact, - onCheckedChange = { eventSink(BugReportEvents.SetCanContact(it)) }, + onCheckedChange = { eventSink(BugReportEvent.SetCanContact(it)) }, enabled = isFormEnabled, title = stringResource(id = R.string.screen_bug_report_contact_me_title), subtitle = stringResource(id = R.string.screen_bug_report_contact_me), @@ -125,7 +125,7 @@ fun BugReportView( if (state.screenshotUri != null) { PreferenceSwitch( isChecked = state.formState.sendScreenshot, - onCheckedChange = { eventSink(BugReportEvents.SetSendScreenshot(it)) }, + onCheckedChange = { eventSink(BugReportEvent.SetSendScreenshot(it)) }, enabled = isFormEnabled, title = stringResource(id = R.string.screen_bug_report_include_screenshot) ) @@ -151,7 +151,7 @@ fun BugReportView( } PreferenceSwitch( isChecked = state.formState.sendPushRules, - onCheckedChange = { eventSink(BugReportEvents.SetSendPushRules(it)) }, + onCheckedChange = { eventSink(BugReportEvent.SetSendPushRules(it)) }, enabled = isFormEnabled, title = stringResource(R.string.screen_bug_report_send_notification_settings_title), subtitle = stringResource(R.string.screen_bug_report_send_notification_settings_description), @@ -179,12 +179,12 @@ fun BugReportView( onValueChange = { if (it.isEmpty()) { ghIssueNumberState = "" - eventSink(BugReportEvents.SetGhIssueNumber(null)) + eventSink(BugReportEvent.SetGhIssueNumber(null)) } else { val number = it.toIntOrNull()?.takeIf { ghInt -> ghInt in 1..99_999 } number?.let { ghIssueNumber -> ghIssueNumberState = ghIssueNumber.toString() - eventSink(BugReportEvents.SetGhIssueNumber(ghIssueNumber)) + eventSink(BugReportEvent.SetGhIssueNumber(ghIssueNumber)) } } }, @@ -202,7 +202,7 @@ fun BugReportView( PreferenceRow { Button( text = stringResource(id = CommonStrings.action_send), - onClick = { eventSink(BugReportEvents.SendBugReport) }, + onClick = { eventSink(BugReportEvent.SendBugReport) }, enabled = state.submitEnabled, showProgress = state.sending.isLoading(), modifier = Modifier @@ -216,7 +216,7 @@ fun BugReportView( async = state.sending, progressDialog = { }, onSuccess = { - eventSink(BugReportEvents.ResetAll) + eventSink(BugReportEvent.ResetAll) onSuccess() }, errorMessage = { error -> @@ -225,14 +225,14 @@ fun BugReportView( else -> error.message ?: error.toString() } }, - onErrorDismiss = { eventSink(BugReportEvents.ClearError) }, + onErrorDismiss = { eventSink(BugReportEvent.ClearError) }, ) } } @Preview(heightDp = 1000) @Composable -internal fun BugReportViewDayPreview(@PreviewParameter(BugReportStateProvider::class) state: BugReportState) = ElementPreview { +internal fun BugReportViewDayPreview(@PreviewParameter(BugReportStatePreviewParam::class) state: BugReportState) = ElementPreview { BugReportView( state = state, onSuccess = {}, @@ -243,7 +243,7 @@ internal fun BugReportViewDayPreview(@PreviewParameter(BugReportStateProvider::c @Preview(heightDp = 1000, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -internal fun BugReportViewNightPreview(@PreviewParameter(BugReportStateProvider::class) state: BugReportState) = ElementPreview { +internal fun BugReportViewNightPreview(@PreviewParameter(BugReportStatePreviewParam::class) state: BugReportState) = ElementPreview { BugReportView( state = state, onSuccess = {}, diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeModule.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeBindingContainer.kt similarity index 97% rename from features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeModule.kt rename to features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeBindingContainer.kt index 475127a345..04edb3c0ac 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeModule.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeBindingContainer.kt @@ -22,7 +22,7 @@ import io.element.android.libraries.architecture.Presenter @ContributesTo(AppScope::class) @BindingContainer -interface RageshakeModule { +interface RageshakeBindingContainer { @Binds fun bindRageshakePreferencesPresenter(presenter: RageshakePreferencesPresenter): Presenter diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt index afbcc939a6..904ff881f0 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt @@ -51,9 +51,9 @@ class BugReportPresenterTest { val presenter = createPresenter() presenter.test { val initialState = awaitItem() - initialState.eventSink.invoke(BugReportEvents.SetDescription(A_SHORT_DESCRIPTION)) + initialState.eventSink.invoke(BugReportEvent.SetDescription(A_SHORT_DESCRIPTION)) assertThat(awaitItem().submitEnabled).isTrue() - initialState.eventSink.invoke(BugReportEvents.SetDescription(A_LONG_DESCRIPTION)) + initialState.eventSink.invoke(BugReportEvent.SetDescription(A_LONG_DESCRIPTION)) assertThat(awaitItem().submitEnabled).isTrue() } } @@ -63,9 +63,9 @@ class BugReportPresenterTest { val presenter = createPresenter() presenter.test { val initialState = awaitItem() - initialState.eventSink.invoke(BugReportEvents.SetCanContact(true)) + initialState.eventSink.invoke(BugReportEvent.SetCanContact(true)) assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(canContact = true)) - initialState.eventSink.invoke(BugReportEvents.SetCanContact(false)) + initialState.eventSink.invoke(BugReportEvent.SetCanContact(false)) assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(canContact = false)) } } @@ -76,9 +76,9 @@ class BugReportPresenterTest { presenter.test { val initialState = awaitItem() // Since this is true by default, start by disabling - initialState.eventSink.invoke(BugReportEvents.SetSendLog(false)) + initialState.eventSink.invoke(BugReportEvent.SetSendLog(false)) assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendLogs = false)) - initialState.eventSink.invoke(BugReportEvents.SetSendLog(true)) + initialState.eventSink.invoke(BugReportEvent.SetSendLog(true)) assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendLogs = true)) } } @@ -88,9 +88,9 @@ class BugReportPresenterTest { val presenter = createPresenter() presenter.test { val initialState = awaitItem() - initialState.eventSink.invoke(BugReportEvents.SetSendScreenshot(true)) + initialState.eventSink.invoke(BugReportEvent.SetSendScreenshot(true)) assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendScreenshot = true)) - initialState.eventSink.invoke(BugReportEvents.SetSendScreenshot(false)) + initialState.eventSink.invoke(BugReportEvent.SetSendScreenshot(false)) assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendScreenshot = false)) } } @@ -100,9 +100,9 @@ class BugReportPresenterTest { val presenter = createPresenter() presenter.test { val initialState = awaitItem() - initialState.eventSink.invoke(BugReportEvents.SetSendPushRules(true)) + initialState.eventSink.invoke(BugReportEvent.SetSendPushRules(true)) assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendPushRules = true)) - initialState.eventSink.invoke(BugReportEvents.SetSendPushRules(false)) + initialState.eventSink.invoke(BugReportEvent.SetSendPushRules(false)) assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(sendPushRules = false)) } } @@ -112,9 +112,9 @@ class BugReportPresenterTest { val presenter = createPresenter() presenter.test { val initialState = awaitItem() - initialState.eventSink.invoke(BugReportEvents.SetGhIssueNumber(1)) + initialState.eventSink.invoke(BugReportEvent.SetGhIssueNumber(1)) assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(ghIssueNumber = 1)) - initialState.eventSink.invoke(BugReportEvents.SetGhIssueNumber(null)) + initialState.eventSink.invoke(BugReportEvent.SetGhIssueNumber(null)) assertThat(awaitItem().formState).isEqualTo(BugReportFormState.Default.copy(ghIssueNumber = null)) } } @@ -130,7 +130,7 @@ class BugReportPresenterTest { val initialState = awaitItem() assertThat(initialState.hasCrashLogs).isTrue() assertThat(initialState.screenshotUri).isEqualTo(A_SCREENSHOT_URI) - initialState.eventSink.invoke(BugReportEvents.ResetAll) + initialState.eventSink.invoke(BugReportEvent.ResetAll) val resetState = awaitItem() assertThat(resetState.hasCrashLogs).isFalse() // TODO Make it live assertThat(resetState.screenshotUri).isNull() @@ -146,9 +146,9 @@ class BugReportPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink.invoke(BugReportEvents.SetDescription(A_LONG_DESCRIPTION)) + initialState.eventSink.invoke(BugReportEvent.SetDescription(A_LONG_DESCRIPTION)) skipItems(1) - initialState.eventSink.invoke(BugReportEvents.SendBugReport) + initialState.eventSink.invoke(BugReportEvent.SendBugReport) skipItems(1) val progressState = awaitItem() assertThat(progressState.sending).isEqualTo(AsyncAction.Loading) @@ -170,9 +170,9 @@ class BugReportPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink.invoke(BugReportEvents.SetDescription(A_LONG_DESCRIPTION)) + initialState.eventSink.invoke(BugReportEvent.SetDescription(A_LONG_DESCRIPTION)) skipItems(1) - initialState.eventSink.invoke(BugReportEvents.SendBugReport) + initialState.eventSink.invoke(BugReportEvent.SendBugReport) skipItems(1) val progressState = awaitItem() assertThat(progressState.sending).isEqualTo(AsyncAction.Loading) @@ -182,7 +182,7 @@ class BugReportPresenterTest { assertThat(awaitItem().sendingProgress).isEqualTo(0f) assertThat((awaitItem().sending as AsyncAction.Failure).error.message).isEqualTo(A_FAILURE_REASON) // Reset failure - initialState.eventSink.invoke(BugReportEvents.ClearError) + initialState.eventSink.invoke(BugReportEvent.ClearError) val lastItem = awaitItem() assertThat(lastItem.sendingProgress).isEqualTo(0f) assertThat(lastItem.sending).isInstanceOf(AsyncAction.Uninitialized::class.java) @@ -194,13 +194,13 @@ class BugReportPresenterTest { val presenter = createPresenter() presenter.test { val initialState = awaitItem() - initialState.eventSink.invoke(BugReportEvents.SetDescription(A_SHORT_DESCRIPTION)) + initialState.eventSink.invoke(BugReportEvent.SetDescription(A_SHORT_DESCRIPTION)) skipItems(1) - initialState.eventSink.invoke(BugReportEvents.SendBugReport) + initialState.eventSink.invoke(BugReportEvent.SendBugReport) val errorState = awaitItem() assertThat(errorState.sending).isEqualTo(AsyncAction.Failure(BugReportFormError.DescriptionTooShort)) // Reset failure - initialState.eventSink.invoke(BugReportEvents.ClearError) + initialState.eventSink.invoke(BugReportEvent.ClearError) val lastItem = awaitItem() assertThat(lastItem.sending).isInstanceOf(AsyncAction.Uninitialized::class.java) } @@ -215,9 +215,9 @@ class BugReportPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink.invoke(BugReportEvents.SetDescription(A_LONG_DESCRIPTION)) + initialState.eventSink.invoke(BugReportEvent.SetDescription(A_LONG_DESCRIPTION)) skipItems(1) - initialState.eventSink.invoke(BugReportEvents.SendBugReport) + initialState.eventSink.invoke(BugReportEvent.SendBugReport) skipItems(1) val progressState = awaitItem() assertThat(progressState.sending).isEqualTo(AsyncAction.Loading) diff --git a/features/rageshake/test/build.gradle.kts b/features/rageshake/test/build.gradle.kts index afc5e51499..faad812d73 100644 --- a/features/rageshake/test/build.gradle.kts +++ b/features/rageshake/test/build.gradle.kts @@ -14,6 +14,7 @@ android { } dependencies { + implementation(projects.libraries.architecture) implementation(projects.features.rageshake.api) implementation(libs.coroutines.core) implementation(projects.tests.testutils) diff --git a/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/FakeBugReportEntryPoint.kt b/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/FakeBugReportEntryPoint.kt new file mode 100644 index 0000000000..d6e1a47368 --- /dev/null +++ b/features/rageshake/test/src/main/kotlin/io/element/android/features/rageshake/test/FakeBugReportEntryPoint.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.rageshake.test + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeBugReportEntryPoint( + private val createNodeResult: (BuildContext, BugReportEntryPoint.Callback) -> Node = { _, _ -> lambdaError() }, +) : BugReportEntryPoint { + override fun createNode( + parentNode: Node, + buildContext: BuildContext, + callback: BugReportEntryPoint.Callback, + ): Node = createNodeResult(buildContext, callback) +} diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomEvents.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomEvent.kt similarity index 65% rename from features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomEvents.kt rename to features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomEvent.kt index f7185f7fcd..e4df50fa7c 100644 --- a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomEvents.kt +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomEvent.kt @@ -8,9 +8,9 @@ package io.element.android.features.reportroom.impl -sealed interface ReportRoomEvents { - data class UpdateReason(val reason: String) : ReportRoomEvents - data object ToggleLeaveRoom : ReportRoomEvents - data object Report : ReportRoomEvents - data object ClearReportAction : ReportRoomEvents +sealed interface ReportRoomEvent { + data class UpdateReason(val reason: String) : ReportRoomEvent + data object ToggleLeaveRoom : ReportRoomEvent + data object Report : ReportRoomEvent + data object ClearReportAction : ReportRoomEvent } diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenter.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenter.kt index 422cf42718..25f9b56d59 100644 --- a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenter.kt +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenter.kt @@ -44,16 +44,16 @@ class ReportRoomPresenter( val coroutineScope = rememberCoroutineScope() - fun handleEvent(event: ReportRoomEvents) { + fun handleEvent(event: ReportRoomEvent) { when (event) { - ReportRoomEvents.Report -> coroutineScope.reportRoom(reason, leaveRoom, reportAction) - ReportRoomEvents.ToggleLeaveRoom -> { + ReportRoomEvent.Report -> coroutineScope.reportRoom(reason, leaveRoom, reportAction) + ReportRoomEvent.ToggleLeaveRoom -> { leaveRoom = !leaveRoom } - is ReportRoomEvents.UpdateReason -> { + is ReportRoomEvent.UpdateReason -> { reason = event.reason } - ReportRoomEvents.ClearReportAction -> { + ReportRoomEvent.ClearReportAction -> { reportAction.value = AsyncAction.Uninitialized } } diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomState.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomState.kt index fa09306991..28427705b0 100644 --- a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomState.kt +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomState.kt @@ -14,7 +14,7 @@ data class ReportRoomState( val reason: String, val leaveRoom: Boolean, val reportAction: AsyncAction, - val eventSink: (ReportRoomEvents) -> Unit + val eventSink: (ReportRoomEvent) -> Unit ) { val canReport: Boolean = reason.isNotBlank() } diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomStateProvider.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomStatePreviewParam.kt similarity index 90% rename from features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomStateProvider.kt rename to features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomStatePreviewParam.kt index 8baae080f5..421c55f0e3 100644 --- a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomStateProvider.kt +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomStatePreviewParam.kt @@ -11,7 +11,7 @@ package io.element.android.features.reportroom.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction -open class ReportRoomStateProvider : PreviewParameterProvider { +open class ReportRoomStatePreviewParam : PreviewParameterProvider { companion object { private const val A_REPORT_ROOM_REASON = "Inappropriate content" } @@ -30,7 +30,7 @@ fun aReportRoomState( reason: String = "", leaveRoom: Boolean = false, reportAction: AsyncAction = AsyncAction.Uninitialized, - eventSink: (ReportRoomEvents) -> Unit = {} + eventSink: (ReportRoomEvent) -> Unit = {} ) = ReportRoomState( reason = reason, leaveRoom = leaveRoom, diff --git a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomView.kt b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomView.kt index a754825dcc..df67b44b22 100644 --- a/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomView.kt +++ b/features/reportroom/impl/src/main/kotlin/io/element/android/features/reportroom/impl/ReportRoomView.kt @@ -66,9 +66,9 @@ fun ReportRoomView( } }, onRetry = { - state.eventSink(ReportRoomEvents.Report) + state.eventSink(ReportRoomEvent.Report) }, - onErrorDismiss = { state.eventSink(ReportRoomEvents.ClearReportAction) } + onErrorDismiss = { state.eventSink(ReportRoomEvent.ClearReportAction) } ) Scaffold( @@ -93,7 +93,7 @@ fun ReportRoomView( ) { TextField( value = state.reason, - onValueChange = { state.eventSink(ReportRoomEvents.UpdateReason(it)) }, + onValueChange = { state.eventSink(ReportRoomEvent.UpdateReason(it)) }, placeholder = stringResource(R.string.screen_report_room_reason_placeholder), minLines = 3, enabled = !isReporting, @@ -112,7 +112,7 @@ fun ReportRoomView( Text(text = stringResource(CommonStrings.action_leave_room)) }, onClick = { - state.eventSink(ReportRoomEvents.ToggleLeaveRoom) + state.eventSink(ReportRoomEvent.ToggleLeaveRoom) }, trailingContent = ListItemContent.Switch(checked = state.leaveRoom) ) @@ -126,7 +126,7 @@ fun ReportRoomView( showProgress = isReporting, onClick = { focusManager.clearFocus(force = true) - state.eventSink(ReportRoomEvents.Report) + state.eventSink(ReportRoomEvent.Report) }, modifier = Modifier .fillMaxWidth() @@ -139,7 +139,7 @@ fun ReportRoomView( @PreviewsDayNight @Composable internal fun ReportRoomViewPreview( - @PreviewParameter(ReportRoomStateProvider::class) state: ReportRoomState + @PreviewParameter(ReportRoomStatePreviewParam::class) state: ReportRoomState ) = ElementPreview { ReportRoomView( state = state, diff --git a/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenterTest.kt b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenterTest.kt index 6bcaeac02d..87b3c49b47 100644 --- a/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenterTest.kt +++ b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomPresenterTest.kt @@ -43,13 +43,13 @@ class ReportRoomPresenterTest { assertThat(state.reason).isEmpty() assertThat(state.canReport).isFalse() assertThat(state.leaveRoom).isFalse() - state.eventSink(ReportRoomEvents.UpdateReason("Spam")) + state.eventSink(ReportRoomEvent.UpdateReason("Spam")) } awaitItem().also { state -> assertThat(state.reason).isEqualTo("Spam") assertThat(state.canReport).isTrue() assertThat(state.leaveRoom).isFalse() - state.eventSink(ReportRoomEvents.ToggleLeaveRoom) + state.eventSink(ReportRoomEvent.ToggleLeaveRoom) } awaitItem().also { state -> assertThat(state.leaveRoom).isTrue() @@ -68,8 +68,8 @@ class ReportRoomPresenterTest { ) val presenter = createReportRoomPresenter(roomId = roomId, reportRoom = reportRoom) presenter.test { - awaitItem().eventSink(ReportRoomEvents.ToggleLeaveRoom) - awaitItem().eventSink(ReportRoomEvents.Report) + awaitItem().eventSink(ReportRoomEvent.ToggleLeaveRoom) + awaitItem().eventSink(ReportRoomEvent.Report) awaitItem().also { state -> assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java) } @@ -93,7 +93,7 @@ class ReportRoomPresenterTest { ) val presenter = createReportRoomPresenter(roomId = roomId, reportRoom = reportRoom) presenter.test { - awaitItem().eventSink(ReportRoomEvents.Report) + awaitItem().eventSink(ReportRoomEvent.Report) awaitItem().also { state -> assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java) } @@ -117,14 +117,14 @@ class ReportRoomPresenterTest { ) val presenter = createReportRoomPresenter(roomId = roomId, reportRoom = reportRoom) presenter.test { - awaitItem().eventSink(ReportRoomEvents.ToggleLeaveRoom) - awaitItem().eventSink(ReportRoomEvents.Report) + awaitItem().eventSink(ReportRoomEvent.ToggleLeaveRoom) + awaitItem().eventSink(ReportRoomEvent.Report) awaitItem().also { state -> assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java) } awaitItem().also { state -> assertThat(state.reportAction).isInstanceOf(AsyncAction.Failure::class.java) - state.eventSink(ReportRoomEvents.Report) + state.eventSink(ReportRoomEvent.Report) } awaitItem().also { state -> assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java) diff --git a/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomViewTest.kt b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomViewTest.kt index 0e43cf6594..64c8744424 100644 --- a/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomViewTest.kt +++ b/features/reportroom/impl/src/test/kotlin/io/element/android/features/reportroom/impl/ReportRoomViewTest.kt @@ -28,7 +28,7 @@ import org.junit.Test class ReportRoomViewTest : RobolectricTest() { @Test fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { setReportRoomView( aReportRoomState( @@ -42,7 +42,7 @@ class ReportRoomViewTest : RobolectricTest() { @Test fun `clicking on report when enabled emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setReportRoomView( aReportRoomState( reason = "Spam", @@ -50,12 +50,12 @@ class ReportRoomViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_report) - eventsRecorder.assertSingle(ReportRoomEvents.Report) + eventsRecorder.assertSingle(ReportRoomEvent.Report) } @Test fun `clicking on decline when disabled does not emit event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) setReportRoomView( aReportRoomState(eventSink = eventsRecorder), ) @@ -64,17 +64,17 @@ class ReportRoomViewTest : RobolectricTest() { @Test fun `clicking on leave room option emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setReportRoomView( aReportRoomState(eventSink = eventsRecorder), ) clickOn(CommonStrings.action_leave_room) - eventsRecorder.assertSingle(ReportRoomEvents.ToggleLeaveRoom) + eventsRecorder.assertSingle(ReportRoomEvent.ToggleLeaveRoom) } @Test fun `typing text in the reason field emits the expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setReportRoomView( aReportRoomState( eventSink = eventsRecorder, @@ -82,7 +82,7 @@ class ReportRoomViewTest : RobolectricTest() { ), ) onNodeWithText("").performTextInput("Spam!") - eventsRecorder.assertSingle(ReportRoomEvents.UpdateReason("Spam!")) + eventsRecorder.assertSingle(ReportRoomEvent.UpdateReason("Spam!")) } } diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsStateProvider.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsStatePreviewParam.kt similarity index 96% rename from features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsStateProvider.kt rename to features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsStatePreviewParam.kt index 700a82795e..95e3d48c2a 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsStateProvider.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsStatePreviewParam.kt @@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.api.room.powerlevels.RoomPowerLevelsV import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableMap -class ChangeRoomPermissionsStateProvider : PreviewParameterProvider { +class ChangeRoomPermissionsStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aChangeRoomPermissionsState(), diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsView.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsView.kt index bb702ed90a..aba7a671a8 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsView.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/permissions/ChangeRoomPermissionsView.kt @@ -132,7 +132,7 @@ private fun titleForType(type: RoomPermissionType): String = when (type) { @PreviewsDayNight @Composable -internal fun ChangeRoomPermissionsViewPreview(@PreviewParameter(ChangeRoomPermissionsStateProvider::class) state: ChangeRoomPermissionsState) { +internal fun ChangeRoomPermissionsViewPreview(@PreviewParameter(ChangeRoomPermissionsStatePreviewParam::class) state: ChangeRoomPermissionsState) { ElementPreview { ChangeRoomPermissionsView( state = state, diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesStateProvider.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesStatePreviewParam.kt similarity index 98% rename from features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesStateProvider.kt rename to features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesStatePreviewParam.kt index e6b71ef752..2d6f8e02d0 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesStateProvider.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesStatePreviewParam.kt @@ -29,7 +29,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -class ChangeRolesStateProvider : PreviewParameterProvider { +class ChangeRolesStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aChangeRolesState(), diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesView.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesView.kt index b2c88efede..05bb77dfd4 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesView.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/roles/ChangeRolesView.kt @@ -393,7 +393,7 @@ private fun MemberRow( @PreviewsDayNight @Composable -internal fun ChangeRolesViewPreview(@PreviewParameter(ChangeRolesStateProvider::class) state: ChangeRolesState) { +internal fun ChangeRolesViewPreview(@PreviewParameter(ChangeRolesStatePreviewParam::class) state: ChangeRolesState) { ElementPreview { ChangeRolesView( state = state diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsEvents.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsEvent.kt similarity index 63% rename from features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsEvents.kt rename to features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsEvent.kt index 69fad02c25..b250fee2d1 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsEvents.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsEvent.kt @@ -10,9 +10,9 @@ package io.element.android.features.rolesandpermissions.impl.root import io.element.android.libraries.matrix.api.room.RoomMember -sealed interface RolesAndPermissionsEvents { - data object ChangeOwnRole : RolesAndPermissionsEvents - data class DemoteSelfTo(val role: RoomMember.Role) : RolesAndPermissionsEvents - data object ResetPermissions : RolesAndPermissionsEvents - data object CancelPendingAction : RolesAndPermissionsEvents +sealed interface RolesAndPermissionsEvent { + data object ChangeOwnRole : RolesAndPermissionsEvent + data class DemoteSelfTo(val role: RoomMember.Role) : RolesAndPermissionsEvent + data object ResetPermissions : RolesAndPermissionsEvent + data object CancelPendingAction : RolesAndPermissionsEvent } diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsPresenter.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsPresenter.kt index dd3a59b99e..1231089af0 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsPresenter.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsPresenter.kt @@ -63,20 +63,20 @@ class RolesAndPermissionsPresenter( val changeOwnRoleAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } val resetPermissionsAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } - fun handleEvent(event: RolesAndPermissionsEvents) { + fun handleEvent(event: RolesAndPermissionsEvent) { when (event) { - is RolesAndPermissionsEvents.ChangeOwnRole -> { + is RolesAndPermissionsEvent.ChangeOwnRole -> { changeOwnRoleAction.value = AsyncAction.ConfirmingNoParams } - is RolesAndPermissionsEvents.CancelPendingAction -> { + is RolesAndPermissionsEvent.CancelPendingAction -> { changeOwnRoleAction.value = AsyncAction.Uninitialized resetPermissionsAction.value = AsyncAction.Uninitialized } - is RolesAndPermissionsEvents.DemoteSelfTo -> coroutineScope.demoteSelfTo( + is RolesAndPermissionsEvent.DemoteSelfTo -> coroutineScope.demoteSelfTo( role = event.role, changeOwnRoleAction = changeOwnRoleAction, ) - is RolesAndPermissionsEvents.ResetPermissions -> if (resetPermissionsAction.value.isConfirming()) { + is RolesAndPermissionsEvent.ResetPermissions -> if (resetPermissionsAction.value.isConfirming()) { coroutineScope.resetPermissions(resetPermissionsAction) } else { resetPermissionsAction.value = AsyncAction.ConfirmingNoParams diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsState.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsState.kt index 626ad3b699..a1345b27c2 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsState.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsState.kt @@ -20,7 +20,7 @@ data class RolesAndPermissionsState( val availableSelfDemoteActions: ImmutableList, val changeOwnRoleAction: AsyncAction, val resetPermissionsAction: AsyncAction, - val eventSink: (RolesAndPermissionsEvents) -> Unit, + val eventSink: (RolesAndPermissionsEvent) -> Unit, ) { val canSelfDemote = availableSelfDemoteActions.isNotEmpty() } diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsStateProvider.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsStatePreviewParam.kt similarity index 94% rename from features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsStateProvider.kt rename to features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsStatePreviewParam.kt index 45bd72db19..2229c185b9 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsStateProvider.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsStatePreviewParam.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction import kotlinx.collections.immutable.toImmutableList -class RolesAndPermissionsStateProvider : PreviewParameterProvider { +class RolesAndPermissionsStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRolesAndPermissionsState(roomSupportsOwners = false), @@ -58,7 +58,7 @@ internal fun aRolesAndPermissionsState( availableSelfDemoteActions: List = listOf(SelfDemoteAction.ToModerator, SelfDemoteAction.ToMember), changeOwnRoleAction: AsyncAction = AsyncAction.Uninitialized, resetPermissionsAction: AsyncAction = AsyncAction.Uninitialized, - eventSink: (RolesAndPermissionsEvents) -> Unit = {}, + eventSink: (RolesAndPermissionsEvent) -> Unit = {}, ) = RolesAndPermissionsState( roomSupportsOwnerRole = roomSupportsOwners, adminCount = adminCount, diff --git a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsView.kt b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsView.kt index 3cf29c18cf..6c9c4396b2 100644 --- a/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsView.kt +++ b/features/rolesandpermissions/impl/src/main/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsView.kt @@ -80,7 +80,7 @@ fun RolesAndPermissionsView( if (state.canSelfDemote) { ListItem( content = { Text(stringResource(R.string.screen_room_roles_and_permissions_change_my_role)) }, - onClick = { state.eventSink(RolesAndPermissionsEvents.ChangeOwnRole) }, + onClick = { state.eventSink(RolesAndPermissionsEvent.ChangeOwnRole) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Edit())) ) } @@ -94,7 +94,7 @@ fun RolesAndPermissionsView( ListItem( content = { Text(stringResource(R.string.screen_room_roles_and_permissions_reset)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Delete())), - onClick = { state.eventSink(RolesAndPermissionsEvents.ResetPermissions) }, + onClick = { state.eventSink(RolesAndPermissionsEvent.ResetPermissions) }, style = ListItemStyle.Destructive, ) } @@ -107,12 +107,12 @@ fun RolesAndPermissionsView( content = stringResource(R.string.screen_room_roles_and_permissions_reset_confirm_description), submitText = stringResource(CommonStrings.action_reset), destructiveSubmit = true, - onSubmitClick = { state.eventSink(RolesAndPermissionsEvents.ResetPermissions) }, - onDismiss = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) }, + onSubmitClick = { state.eventSink(RolesAndPermissionsEvent.ResetPermissions) }, + onDismiss = { state.eventSink(RolesAndPermissionsEvent.CancelPendingAction) }, ) }, - onSuccess = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) }, - onErrorDismiss = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) } + onSuccess = { state.eventSink(RolesAndPermissionsEvent.CancelPendingAction) }, + onErrorDismiss = { state.eventSink(RolesAndPermissionsEvent.CancelPendingAction) } ) when (state.changeOwnRoleAction) { @@ -128,7 +128,7 @@ fun RolesAndPermissionsView( is AsyncAction.Failure -> { ErrorDialog( content = stringResource(CommonStrings.error_unknown), - onSubmit = { state.eventSink(RolesAndPermissionsEvents.CancelPendingAction) } + onSubmit = { state.eventSink(RolesAndPermissionsEvent.CancelPendingAction) } ) } else -> Unit @@ -139,7 +139,7 @@ fun RolesAndPermissionsView( @Composable private fun ChangeOwnRoleBottomSheet( availableDemoteActions: ImmutableList, - eventSink: (RolesAndPermissionsEvents) -> Unit, + eventSink: (RolesAndPermissionsEvent) -> Unit, ) { val coroutineScope = rememberCoroutineScope() val sheetState = rememberBottomSheetState( @@ -148,7 +148,7 @@ private fun ChangeOwnRoleBottomSheet( ) fun dismiss() { sheetState.hide(coroutineScope) { - eventSink(RolesAndPermissionsEvents.CancelPendingAction) + eventSink(RolesAndPermissionsEvent.CancelPendingAction) } } ModalBottomSheet( @@ -176,7 +176,7 @@ private fun ChangeOwnRoleBottomSheet( content = { Text(stringResource(demoteAction.titleRes)) }, onClick = { sheetState.hide(coroutineScope) { - eventSink(RolesAndPermissionsEvents.DemoteSelfTo(demoteAction.role)) + eventSink(RolesAndPermissionsEvent.DemoteSelfTo(demoteAction.role)) } }, style = ListItemStyle.Destructive, @@ -191,7 +191,7 @@ private fun ChangeOwnRoleBottomSheet( @PreviewsDayNight @Composable -internal fun RolesAndPermissionsViewPreview(@PreviewParameter(RolesAndPermissionsStateProvider::class) state: RolesAndPermissionsState) { +internal fun RolesAndPermissionsViewPreview(@PreviewParameter(RolesAndPermissionsStatePreviewParam::class) state: RolesAndPermissionsState) { ElementPreview { RolesAndPermissionsView( state = state, diff --git a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionPresenterTest.kt b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionPresenterTest.kt index 54bd6b7987..29ba0d368a 100644 --- a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionPresenterTest.kt +++ b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionPresenterTest.kt @@ -45,7 +45,7 @@ class RolesAndPermissionPresenterTest { val presenter = createRolesAndPermissionsPresenter() presenter.test { val initialState = awaitItem() - initialState.eventSink(RolesAndPermissionsEvents.ChangeOwnRole) + initialState.eventSink(RolesAndPermissionsEvent.ChangeOwnRole) assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.ConfirmingNoParams) } } @@ -62,7 +62,7 @@ class RolesAndPermissionPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.Moderator)) + initialState.eventSink(RolesAndPermissionsEvent.DemoteSelfTo(RoomMember.Role.Moderator)) runCurrent() assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.Loading) @@ -82,7 +82,7 @@ class RolesAndPermissionPresenterTest { val presenter = createRolesAndPermissionsPresenter(room = room, dispatchers = testCoroutineDispatchers()) presenter.test { val initialState = awaitItem() - initialState.eventSink(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.Moderator)) + initialState.eventSink(RolesAndPermissionsEvent.DemoteSelfTo(RoomMember.Role.Moderator)) runCurrent() assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.Loading) @@ -90,7 +90,7 @@ class RolesAndPermissionPresenterTest { runCurrent() assertThat(awaitItem().changeOwnRoleAction).isInstanceOf(AsyncAction.Failure::class.java) - initialState.eventSink(RolesAndPermissionsEvents.CancelPendingAction) + initialState.eventSink(RolesAndPermissionsEvent.CancelPendingAction) assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.Uninitialized) } } @@ -100,8 +100,8 @@ class RolesAndPermissionPresenterTest { val presenter = createRolesAndPermissionsPresenter() presenter.test { val initialState = awaitItem() - initialState.eventSink(RolesAndPermissionsEvents.ChangeOwnRole) - awaitItem().eventSink(RolesAndPermissionsEvents.CancelPendingAction) + initialState.eventSink(RolesAndPermissionsEvent.ChangeOwnRole) + awaitItem().eventSink(RolesAndPermissionsEvent.CancelPendingAction) assertThat(awaitItem().changeOwnRoleAction).isEqualTo(AsyncAction.Uninitialized) } @@ -119,9 +119,9 @@ class RolesAndPermissionPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink(RolesAndPermissionsEvents.ResetPermissions) + initialState.eventSink(RolesAndPermissionsEvent.ResetPermissions) // Confirmation - awaitItem().eventSink(RolesAndPermissionsEvents.ResetPermissions) + awaitItem().eventSink(RolesAndPermissionsEvent.ResetPermissions) assertThat(awaitItem().resetPermissionsAction).isEqualTo(AsyncAction.Loading) assertThat(awaitItem().resetPermissionsAction).isEqualTo(AsyncAction.Success(Unit)) @@ -134,8 +134,8 @@ class RolesAndPermissionPresenterTest { val presenter = createRolesAndPermissionsPresenter() presenter.test { val initialState = awaitItem() - initialState.eventSink(RolesAndPermissionsEvents.ResetPermissions) - awaitItem().eventSink(RolesAndPermissionsEvents.CancelPendingAction) + initialState.eventSink(RolesAndPermissionsEvent.ResetPermissions) + awaitItem().eventSink(RolesAndPermissionsEvent.CancelPendingAction) assertThat(awaitItem().resetPermissionsAction).isEqualTo(AsyncAction.Uninitialized) } diff --git a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsViewTest.kt b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsViewTest.kt index 8a3b6fd766..7e94468d48 100644 --- a/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsViewTest.kt +++ b/features/rolesandpermissions/impl/src/test/kotlin/io/element/android/features/rolesandpermissions/impl/root/RolesAndPermissionsViewTest.kt @@ -92,19 +92,19 @@ class RolesAndPermissionsViewTest : RobolectricTest() { @Test @Config(qualifiers = "h640dp") fun `tapping on reset permissions triggers ResetPermissions event`() = runAndroidComposeUiTest { - val recorder = EventsRecorder() + val recorder = EventsRecorder() setRolesAndPermissionsView( state = aRolesAndPermissionsState( eventSink = recorder, ), ) clickOn(R.string.screen_room_roles_and_permissions_reset) - recorder.assertSingle(RolesAndPermissionsEvents.ResetPermissions) + recorder.assertSingle(RolesAndPermissionsEvent.ResetPermissions) } @Test fun `tapping on Reset in the reset permissions confirmation dialog triggers ResetPermissions event`() = runAndroidComposeUiTest { - val recorder = EventsRecorder() + val recorder = EventsRecorder() setRolesAndPermissionsView( state = aRolesAndPermissionsState( resetPermissionsAction = AsyncAction.ConfirmingNoParams, @@ -112,12 +112,12 @@ class RolesAndPermissionsViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_reset) - recorder.assertSingle(RolesAndPermissionsEvents.ResetPermissions) + recorder.assertSingle(RolesAndPermissionsEvent.ResetPermissions) } @Test fun `tapping on Cancel in the reset permissions confirmation dialog triggers CancelPendingAction event`() = runAndroidComposeUiTest { - val recorder = EventsRecorder() + val recorder = EventsRecorder() setRolesAndPermissionsView( state = aRolesAndPermissionsState( resetPermissionsAction = AsyncAction.ConfirmingNoParams, @@ -125,12 +125,12 @@ class RolesAndPermissionsViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_cancel) - recorder.assertSingle(RolesAndPermissionsEvents.CancelPendingAction) + recorder.assertSingle(RolesAndPermissionsEvent.CancelPendingAction) } @Test fun `tapping on 'Demote to moderator' in the demote self bottom sheet triggers the right event`() = runAndroidComposeUiTest { - val recorder = EventsRecorder() + val recorder = EventsRecorder() setRolesAndPermissionsView( state = aRolesAndPermissionsState( changeOwnRoleAction = AsyncAction.ConfirmingNoParams, @@ -139,12 +139,12 @@ class RolesAndPermissionsViewTest : RobolectricTest() { ) clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_moderator) mainClock.advanceTimeBy(1_000L) - recorder.assertSingle(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.Moderator)) + recorder.assertSingle(RolesAndPermissionsEvent.DemoteSelfTo(RoomMember.Role.Moderator)) } @Test fun `tapping on 'Demote to member' in the demote self bottom sheet triggers the right event`() = runAndroidComposeUiTest { - val recorder = EventsRecorder() + val recorder = EventsRecorder() setRolesAndPermissionsView( state = aRolesAndPermissionsState( changeOwnRoleAction = AsyncAction.ConfirmingNoParams, @@ -153,12 +153,12 @@ class RolesAndPermissionsViewTest : RobolectricTest() { ) clickOn(R.string.screen_room_roles_and_permissions_change_role_demote_to_member) mainClock.advanceTimeBy(1_000L) - recorder.assertSingle(RolesAndPermissionsEvents.DemoteSelfTo(RoomMember.Role.User)) + recorder.assertSingle(RolesAndPermissionsEvent.DemoteSelfTo(RoomMember.Role.User)) } @Test fun `tapping on 'Cancel' in the demote self bottom sheet triggers the right event`() = runAndroidComposeUiTest { - val recorder = EventsRecorder() + val recorder = EventsRecorder() setRolesAndPermissionsView( state = aRolesAndPermissionsState( changeOwnRoleAction = AsyncAction.ConfirmingNoParams, @@ -167,7 +167,7 @@ class RolesAndPermissionsViewTest : RobolectricTest() { ) clickOn(CommonStrings.action_cancel) mainClock.advanceTimeBy(1_000L) - recorder.assertSingle(RolesAndPermissionsEvents.CancelPendingAction) + recorder.assertSingle(RolesAndPermissionsEvent.CancelPendingAction) } } diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverEvents.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverEvent.kt similarity index 67% rename from features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverEvents.kt rename to features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverEvent.kt index 11b92ba244..6c528cea14 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverEvents.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverEvent.kt @@ -8,7 +8,7 @@ package io.element.android.features.roomaliasresolver.impl -sealed interface RoomAliasResolverEvents { - data object Retry : RoomAliasResolverEvents - data object DismissError : RoomAliasResolverEvents +sealed interface RoomAliasResolverEvent { + data object Retry : RoomAliasResolverEvent + data object DismissError : RoomAliasResolverEvent } diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenter.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenter.kt index 78be168e53..41a49d0fe1 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenter.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverPresenter.kt @@ -24,7 +24,10 @@ import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull +import java.util.concurrent.TimeoutException import kotlin.jvm.optionals.getOrElse +import kotlin.time.Duration.Companion.seconds @AssistedInject class RoomAliasResolverPresenter( @@ -45,10 +48,10 @@ class RoomAliasResolverPresenter( resolveAlias(resolveState) } - fun handleEvent(event: RoomAliasResolverEvents) { + fun handleEvent(event: RoomAliasResolverEvent) { when (event) { - RoomAliasResolverEvents.Retry -> coroutineScope.resolveAlias(resolveState) - RoomAliasResolverEvents.DismissError -> resolveState.value = AsyncData.Uninitialized + RoomAliasResolverEvent.Retry -> coroutineScope.resolveAlias(resolveState) + RoomAliasResolverEvent.DismissError -> resolveState.value = AsyncData.Uninitialized } } @@ -61,9 +64,14 @@ class RoomAliasResolverPresenter( private fun CoroutineScope.resolveAlias(resolveState: MutableState>) = launch { suspend { - matrixClient.resolveRoomAlias(roomAlias) + val result = withTimeoutOrNull(ALIAS_RESOLVE_TIMEOUT_IN_SECONDS.seconds) { + matrixClient.resolveRoomAlias(roomAlias) + } ?: throw TimeoutException("Timed out while resolving ${roomAlias.value}") + result .getOrThrow() .getOrElse { throw RoomAliasResolverFailures.UnknownAlias } }.runCatchingUpdatingState(resolveState) } } + +private const val ALIAS_RESOLVE_TIMEOUT_IN_SECONDS = 10 diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverState.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverState.kt index 82d13f16a5..1974fdff7c 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverState.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverState.kt @@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias data class RoomAliasResolverState( val roomAlias: RoomAlias, val resolveState: AsyncData, - val eventSink: (RoomAliasResolverEvents) -> Unit + val eventSink: (RoomAliasResolverEvent) -> Unit ) sealed class RoomAliasResolverFailures : Exception() { diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverStateProvider.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverStatePreviewParam.kt similarity index 90% rename from features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverStateProvider.kt rename to features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverStatePreviewParam.kt index c3152bc303..1bb4e03a45 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverStateProvider.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverStatePreviewParam.kt @@ -14,7 +14,7 @@ import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.exception.ClientException import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias -open class RoomAliasResolverStateProvider : PreviewParameterProvider { +open class RoomAliasResolverStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomAliasResolverState(), @@ -30,7 +30,7 @@ open class RoomAliasResolverStateProvider : PreviewParameterProvider = AsyncData.Uninitialized, - eventSink: (RoomAliasResolverEvents) -> Unit = {} + eventSink: (RoomAliasResolverEvent) -> Unit = {} ) = RoomAliasResolverState( roomAlias = roomAlias, resolveState = resolveState, diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverView.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverView.kt index ad0d2e9ab9..dd002814b1 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverView.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasResolverView.kt @@ -66,9 +66,9 @@ fun RoomAliasResolverView( ResolvedRoomAliasView( resolvedRoomAlias = state.resolveState, onSuccess = onSuccess, - onRetry = { state.eventSink(RoomAliasResolverEvents.Retry) }, + onRetry = { state.eventSink(RoomAliasResolverEvent.Retry) }, onDismissError = { - state.eventSink(RoomAliasResolverEvents.DismissError) + state.eventSink(RoomAliasResolverEvent.DismissError) onBackClick() } ) @@ -147,7 +147,7 @@ private fun RoomAliasResolverTopBar( @PreviewsDayNight @Composable -internal fun RoomAliasResolverViewPreview(@PreviewParameter(RoomAliasResolverStateProvider::class) state: RoomAliasResolverState) = ElementPreview { +internal fun RoomAliasResolverViewPreview(@PreviewParameter(RoomAliasResolverStatePreviewParam::class) state: RoomAliasResolverState) = ElementPreview { RoomAliasResolverView( state = state, onSuccess = { }, diff --git a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/di/RoomAliasResolverModule.kt b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/di/RoomAliasResolverBindingContainer.kt similarity index 92% rename from features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/di/RoomAliasResolverModule.kt rename to features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/di/RoomAliasResolverBindingContainer.kt index 8f5c9d733b..a3796fad91 100644 --- a/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/di/RoomAliasResolverModule.kt +++ b/features/roomaliasresolver/impl/src/main/kotlin/io/element/android/features/roomaliasresolver/impl/di/RoomAliasResolverBindingContainer.kt @@ -18,9 +18,9 @@ import io.element.android.libraries.matrix.api.core.RoomAlias @BindingContainer @ContributesTo(SessionScope::class) -object RoomAliasResolverModule { +object RoomAliasResolverBindingContainer { @Provides - fun providesJoinRoomPresenterFactory( + fun providesRoomAliasResolverPresenterFactory( client: MatrixClient, ): RoomAliasResolverPresenter.Factory { return object : RoomAliasResolverPresenter.Factory { diff --git a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperPresenterTest.kt b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperPresenterTest.kt index df8b8058be..d480ffce11 100644 --- a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperPresenterTest.kt +++ b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperPresenterTest.kt @@ -22,10 +22,12 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SERVER_LIST import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.tests.testutils.WarmUpRule +import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test import java.util.Optional +import java.util.concurrent.TimeoutException class RoomAliasHelperPresenterTest { @get:Rule @@ -60,6 +62,22 @@ class RoomAliasHelperPresenterTest { } } + @Test + fun `present - a server that never answers ends up in an error state`() = runTest { + val client = FakeMatrixClient( + resolveRoomAliasResult = { awaitCancellation() } + ) + val presenter = createPresenter(matrixClient = client) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + assertThat(awaitItem().resolveState.isUninitialized()).isTrue() + assertThat(awaitItem().resolveState.isLoading()).isTrue() + val resultState = awaitItem() + assertThat(resultState.resolveState.errorOrNull()).isInstanceOf(TimeoutException::class.java) + } + } + @Test fun `present - resolve alias error and retry`() = runTest { val client = FakeMatrixClient( @@ -73,7 +91,7 @@ class RoomAliasHelperPresenterTest { assertThat(awaitItem().resolveState.isLoading()).isTrue() val resultState = awaitItem() assertThat(resultState.resolveState.errorOrNull()).isEqualTo(AN_EXCEPTION) - resultState.eventSink(RoomAliasResolverEvents.Retry) + resultState.eventSink(RoomAliasResolverEvent.Retry) val retryLoadingState = awaitItem() assertThat(retryLoadingState.resolveState.isLoading()).isTrue() val retryState = awaitItem() diff --git a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperViewTest.kt b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperViewTest.kt index ae33517732..18a97ebb69 100644 --- a/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperViewTest.kt +++ b/features/roomaliasresolver/impl/src/test/kotlin/io/element/android/features/roomaliasresolver/impl/RoomAliasHelperViewTest.kt @@ -30,7 +30,7 @@ import org.junit.Test class RoomAliasHelperViewTest : RobolectricTest() { @Test fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { setRoomAliasResolverView( aRoomAliasResolverState( @@ -44,7 +44,7 @@ class RoomAliasHelperViewTest : RobolectricTest() { @Test fun `clicking on Retry emits the expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setRoomAliasResolverView( aRoomAliasResolverState( resolveState = AsyncData.Failure(Exception("Error")), @@ -52,13 +52,13 @@ class RoomAliasHelperViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_retry) - eventsRecorder.assertSingle(RoomAliasResolverEvents.Retry) + eventsRecorder.assertSingle(RoomAliasResolverEvent.Retry) } @Test fun `success state invokes the expected Callback`() = runAndroidComposeUiTest { val result = aResolvedRoomAlias() - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(result) { setRoomAliasResolverView( aRoomAliasResolverState( diff --git a/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt b/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStatePreviewParam.kt similarity index 94% rename from features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt rename to features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStatePreviewParam.kt index 3356fa0366..84b0bda6be 100644 --- a/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStateProvider.kt +++ b/features/roomcall/api/src/main/kotlin/io/element/android/features/roomcall/api/RoomCallStatePreviewParam.kt @@ -10,7 +10,7 @@ package io.element.android.features.roomcall.api import androidx.compose.ui.tooling.preview.PreviewParameterProvider -open class RoomCallStateProvider : PreviewParameterProvider { +open class RoomCallStatePreviewParam : PreviewParameterProvider { override val values: Sequence = sequenceOf( aStandByCallState(), aStandByCallState(canStartCall = false), diff --git a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/di/RoomCallModule.kt b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/di/RoomCallBindingContainer.kt similarity index 95% rename from features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/di/RoomCallModule.kt rename to features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/di/RoomCallBindingContainer.kt index fc649dd1f2..aa2d0fdde4 100644 --- a/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/di/RoomCallModule.kt +++ b/features/roomcall/impl/src/main/kotlin/io/element/android/features/roomcall/impl/di/RoomCallBindingContainer.kt @@ -18,7 +18,7 @@ import io.element.android.libraries.di.RoomScope @ContributesTo(RoomScope::class) @BindingContainer -interface RoomCallModule { +interface RoomCallBindingContainer { @Binds fun bindRoomCallStatePresenter(presenter: RoomCallStatePresenter): Presenter } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStatePreviewParam.kt similarity index 99% rename from features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt rename to features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStatePreviewParam.kt index 6aa4f11a8e..d1652b30dd 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsStatePreviewParam.kt @@ -31,7 +31,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUserList import kotlinx.collections.immutable.toImmutableList -open class RoomDetailsStateProvider : PreviewParameterProvider { +open class RoomDetailsStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomDetailsState(displayAdminSettings = true), diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index 96dc3e25ba..b2874d01f9 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -905,12 +905,12 @@ private fun DebugInfoSection( @PreviewWithExtraLargeHeight @Composable -internal fun RoomDetailsPreview(@PreviewParameter(RoomDetailsStateProvider::class) state: RoomDetailsState) = +internal fun RoomDetailsPreview(@PreviewParameter(RoomDetailsStatePreviewParam::class) state: RoomDetailsState) = ElementPreviewLight { ContentToPreview(state) } @PreviewWithExtraLargeHeight @Composable -internal fun RoomDetailsDarkPreview(@PreviewParameter(RoomDetailsStateProvider::class) state: RoomDetailsState) = +internal fun RoomDetailsDarkPreview(@PreviewParameter(RoomDetailsStatePreviewParam::class) state: RoomDetailsState) = ElementPreviewDark { ContentToPreview(state) } @PreviewWithExtraLargeHeight diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberBindingContainer.kt similarity index 95% rename from features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt rename to features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberBindingContainer.kt index 479f23f90d..6651e85097 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberBindingContainer.kt @@ -21,9 +21,9 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom @BindingContainer @ContributesTo(RoomScope::class) -object RoomMemberModule { +object RoomMemberBindingContainer { @Provides - fun provideRoomMemberDetailsPresenterFactory( + fun providesRoomMemberDetailsPresenterFactory( room: JoinedRoom, userProfilePresenterFactory: UserProfilePresenterFactory, encryptionService: EncryptionService, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt index 3919817313..87d0ca7ace 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt @@ -20,7 +20,7 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.annotations.ContributesNode -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.InvitePeopleRenderer import io.element.android.libraries.architecture.callback @@ -81,7 +81,7 @@ class RoomInviteMembersNode( ProgressDialog(text = stringResource(CommonStrings.common_creating_room)) }, onErrorDismiss = { - state.eventSink(InvitePeopleEvents.ClearError) + state.eventSink(InvitePeopleEvent.ClearError) } ) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt index cb305426bc..ba9d2b4498 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt @@ -22,9 +22,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme -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.features.roomdetails.impl.R import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.button.BackButton @@ -49,13 +49,13 @@ fun RoomInviteMembersView( RoomInviteMembersTopBar( onBackClick = { if (state.isSearchActive) { - state.eventSink(InvitePeopleEvents.CloseSearch) + state.eventSink(InvitePeopleEvent.CloseSearch) } else { onBackClick() } }, onSubmitClick = { - state.eventSink(InvitePeopleEvents.SendInvites) + state.eventSink(InvitePeopleEvent.SendInvites) }, canSend = state.canInvite, ) @@ -116,7 +116,7 @@ private fun InviteProgressDialog() { @PreviewsDayNight @Composable -internal fun RoomInviteMembersViewPreview(@PreviewParameter(InvitePeopleStateProvider::class) state: InvitePeopleState) = ElementPreview { +internal fun RoomInviteMembersViewPreview(@PreviewParameter(InvitePeopleStatePreviewParam::class) state: InvitePeopleState) = ElementPreview { RoomInviteMembersView( state = state, invitePeopleView = {}, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt index 37f107e784..402ff5ed02 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -21,7 +21,7 @@ import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.MobileScreen import io.element.android.annotations.ContributesNode import io.element.android.features.roommembermoderation.api.ModerationAction -import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent import io.element.android.features.roommembermoderation.api.RoomMemberModerationRenderer import io.element.android.libraries.architecture.appyx.launchMolecule import io.element.android.libraries.architecture.callback @@ -81,7 +81,7 @@ class RoomMemberListNode( onSelectAction = { action, target -> when (action) { is ModerationAction.DisplayProfile -> openRoomMemberDetails(target.userId) - else -> state.moderationState.eventSink(RoomMemberModerationEvents.ProcessAction(action, target)) + else -> state.moderationState.eventSink(RoomMemberModerationEvent.ProcessAction(action, target)) } }, onAvatarClick = { user -> diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 674dd9aad9..a47f9af84f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -19,7 +19,7 @@ import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject -import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents.ShowActionsForUser +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent.ShowActionsForUser import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStatePreviewParam.kt similarity index 97% rename from features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt rename to features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStatePreviewParam.kt index 6b1971fdae..39e6ffdace 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListStatePreviewParam.kt @@ -10,7 +10,7 @@ package io.element.android.features.roomdetails.impl.members import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncData @@ -31,7 +31,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.user.DisplayedStatus import kotlinx.collections.immutable.persistentListOf -internal class RoomMemberListStateProvider : PreviewParameterProvider { +internal class RoomMemberListStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomMemberListState( @@ -115,7 +115,7 @@ fun aRoomMemberModerationState( canBan = canBan, canKick = canKick, ) - override val eventSink: (RoomMemberModerationEvents) -> Unit = {} + override val eventSink: (RoomMemberModerationEvent) -> Unit = {} } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt index 33ca61b570..f19ee95106 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt @@ -375,7 +375,7 @@ private fun RoomMemberListTopBar( @PreviewsDayNight @Composable -internal fun RoomMemberListViewPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) = ElementPreview { +internal fun RoomMemberListViewPreview(@PreviewParameter(RoomMemberListStatePreviewParam::class) state: RoomMemberListState) = ElementPreview { RoomMemberListView( state = state, navigator = object : RoomMemberListNavigator { diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt index 9b18c88c93..85eca4fbae 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt @@ -17,7 +17,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject -import io.element.android.features.userprofile.api.UserProfileEvents +import io.element.android.features.userprofile.api.UserProfileEvent import io.element.android.features.userprofile.api.UserProfilePresenterFactory import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.api.UserProfileVerificationState @@ -112,12 +112,12 @@ class RoomMemberDetailsPresenter( } } - fun handleEvent(event: UserProfileEvents) { + fun handleEvent(event: UserProfileEvent) { when (event) { - UserProfileEvents.WithdrawVerification -> coroutineScope.launch { + UserProfileEvent.WithdrawVerification -> coroutineScope.launch { encryptionService.withdrawVerification(roomMemberId) } - is UserProfileEvents.CopyToClipboard -> { + is UserProfileEvent.CopyToClipboard -> { clipboardHelper.copyPlainText(event.text) snackbarDispatcher.post(SnackbarMessage(CommonStrings.common_copied_to_clipboard)) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStatePreviewParam.kt similarity index 95% rename from features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt rename to features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStatePreviewParam.kt index 603d22b502..f7ac7597d6 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsStatePreviewParam.kt @@ -14,7 +14,7 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.room.RoomNotificationMode -internal class RoomNotificationSettingsStateProvider : PreviewParameterProvider { +internal class RoomNotificationSettingsStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomNotificationSettingsState(), diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt index a8be2944a2..f0676f133e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/RoomNotificationSettingsView.kt @@ -174,7 +174,7 @@ private fun RoomNotificationSettingsTopBar( @PreviewsDayNight @Composable internal fun RoomNotificationSettingsViewPreview( - @PreviewParameter(RoomNotificationSettingsStateProvider::class) state: RoomNotificationSettingsState + @PreviewParameter(RoomNotificationSettingsStatePreviewParam::class) state: RoomNotificationSettingsState ) = ElementPreview { RoomNotificationSettingsView( state = state, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsStatePreviewParam.kt similarity index 92% rename from features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsStateProvider.kt rename to features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsStatePreviewParam.kt index 039f489bbf..6a3ecacfcb 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsStateProvider.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsStatePreviewParam.kt @@ -14,7 +14,7 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.room.RoomNotificationMode -internal class UserDefinedRoomNotificationSettingsStateProvider : PreviewParameterProvider { +internal class UserDefinedRoomNotificationSettingsStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( RoomNotificationSettingsState( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt index 1721c9925c..befbc9382f 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/notificationsettings/UserDefinedRoomNotificationSettingsView.kt @@ -105,7 +105,7 @@ private fun UserDefinedRoomNotificationSettingsTopBar( @PreviewsDayNight @Composable internal fun UserDefinedRoomNotificationSettingsViewPreview( - @PreviewParameter(UserDefinedRoomNotificationSettingsStateProvider::class) state: RoomNotificationSettingsState + @PreviewParameter(UserDefinedRoomNotificationSettingsStatePreviewParam::class) state: RoomNotificationSettingsState ) = ElementPreview { UserDefinedRoomNotificationSettingsView( state = state, diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenterTest.kt index 34ac8d623f..53ec3b1491 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenterTest.kt @@ -11,7 +11,7 @@ package io.element.android.features.roomdetails.impl.members.details import com.google.common.truth.Truth.assertThat import io.element.android.features.roomdetails.impl.aJoinedRoom import io.element.android.features.roomdetails.impl.members.aRoomMember -import io.element.android.features.userprofile.api.UserProfileEvents +import io.element.android.features.userprofile.api.UserProfileEvent import io.element.android.features.userprofile.api.UserProfilePresenterFactory import io.element.android.features.userprofile.api.UserProfileVerificationState import io.element.android.features.userprofile.shared.aUserProfileState @@ -348,7 +348,7 @@ class RoomMemberDetailsPresenterTest { consumeItemsUntilPredicate { it.verificationState == UserProfileVerificationState.VERIFICATION_VIOLATION } - initialState.eventSink(UserProfileEvents.WithdrawVerification) + initialState.eventSink(UserProfileEvent.WithdrawVerification) withdrawVerificationResult.assertions().isCalledOnce() } } diff --git a/features/roomdetailsedit/impl/src/main/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditStateProvider.kt b/features/roomdetailsedit/impl/src/main/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditStatePreviewParam.kt similarity index 96% rename from features/roomdetailsedit/impl/src/main/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditStateProvider.kt rename to features/roomdetailsedit/impl/src/main/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditStatePreviewParam.kt index a6e2e3798b..b58cf5db49 100644 --- a/features/roomdetailsedit/impl/src/main/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditStateProvider.kt +++ b/features/roomdetailsedit/impl/src/main/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditStatePreviewParam.kt @@ -16,7 +16,7 @@ import io.element.android.libraries.permissions.api.PermissionsState import io.element.android.libraries.permissions.api.aPermissionsState import kotlinx.collections.immutable.toImmutableList -open class RoomDetailsEditStateProvider : PreviewParameterProvider { +open class RoomDetailsEditStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomDetailsEditState(), diff --git a/features/roomdetailsedit/impl/src/main/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditView.kt b/features/roomdetailsedit/impl/src/main/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditView.kt index 9cdc800267..04c8796285 100644 --- a/features/roomdetailsedit/impl/src/main/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditView.kt +++ b/features/roomdetailsedit/impl/src/main/kotlin/io/element/android/features/roomdetailsedit/impl/RoomDetailsEditView.kt @@ -185,7 +185,7 @@ fun RoomDetailsEditView( @PreviewsDayNight @Composable -internal fun RoomDetailsEditViewPreview(@PreviewParameter(RoomDetailsEditStateProvider::class) state: RoomDetailsEditState) = ElementPreview { +internal fun RoomDetailsEditViewPreview(@PreviewParameter(RoomDetailsEditStatePreviewParam::class) state: RoomDetailsEditState) = ElementPreview { RoomDetailsEditView( state = state, onDone = {}, diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvent.kt similarity index 67% rename from features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt rename to features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvent.kt index 20e866f2bf..8eb82d8ea9 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvents.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryEvent.kt @@ -8,7 +8,7 @@ package io.element.android.features.roomdirectory.impl.root -sealed interface RoomDirectoryEvents { - data class Search(val query: String) : RoomDirectoryEvents - data object LoadMore : RoomDirectoryEvents +sealed interface RoomDirectoryEvent { + data class Search(val query: String) : RoomDirectoryEvent + data object LoadMore : RoomDirectoryEvent } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt index 98338440a1..b99a31dced 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenter.kt @@ -63,12 +63,12 @@ class RoomDirectoryPresenter( loadingMore = false } } - fun handleEvent(event: RoomDirectoryEvents) { + fun handleEvent(event: RoomDirectoryEvent) { when (event) { - RoomDirectoryEvents.LoadMore -> { + RoomDirectoryEvent.LoadMore -> { loadingMore = true } - is RoomDirectoryEvents.Search -> { + is RoomDirectoryEvent.Search -> { searchQuery = event.query } } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt index a3647381dc..4066ff785a 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryState.kt @@ -15,7 +15,7 @@ data class RoomDirectoryState( val query: String, val roomDescriptions: ImmutableList, val displayLoadMoreIndicator: Boolean, - val eventSink: (RoomDirectoryEvents) -> Unit + val eventSink: (RoomDirectoryEvent) -> Unit ) { val displayEmptyState = roomDescriptions.isEmpty() && !displayLoadMoreIndicator } diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStatePreviewParam.kt similarity index 94% rename from features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt rename to features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStatePreviewParam.kt index 4ee90fdf16..89dde9605b 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStateProvider.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryStatePreviewParam.kt @@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -open class RoomDirectoryStateProvider : PreviewParameterProvider { +open class RoomDirectoryStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomDirectoryState(), @@ -35,7 +35,7 @@ fun aRoomDirectoryState( query: String = "", displayLoadMoreIndicator: Boolean = false, roomDescriptions: ImmutableList = persistentListOf(), - eventSink: (RoomDirectoryEvents) -> Unit = {}, + eventSink: (RoomDirectoryEvent) -> Unit = {}, ) = RoomDirectoryState( query = query, roomDescriptions = roomDescriptions, diff --git a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt index dd9098b7ad..3e9c6e05c4 100644 --- a/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt +++ b/features/roomdirectory/impl/src/main/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryView.kt @@ -107,7 +107,7 @@ private fun RoomDirectoryContent( Column(modifier = modifier) { SearchTextField( query = state.query, - onQueryChange = { state.eventSink(RoomDirectoryEvents.Search(it)) }, + onQueryChange = { state.eventSink(RoomDirectoryEvent.Search(it)) }, placeholder = stringResource(id = CommonStrings.action_search), modifier = Modifier.fillMaxWidth(), ) @@ -116,7 +116,7 @@ private fun RoomDirectoryContent( displayLoadMoreIndicator = state.displayLoadMoreIndicator, displayEmptyState = state.displayEmptyState, onResultClick = onResultClick, - onReachedLoadMore = { state.eventSink(RoomDirectoryEvents.LoadMore) }, + onReachedLoadMore = { state.eventSink(RoomDirectoryEvent.LoadMore) }, ) } } @@ -278,7 +278,7 @@ private fun RoomDirectoryRoomRow( @PreviewsDayNight @Composable -internal fun RoomDirectoryViewPreview(@PreviewParameter(RoomDirectoryStateProvider::class) state: RoomDirectoryState) = ElementPreview { +internal fun RoomDirectoryViewPreview(@PreviewParameter(RoomDirectoryStatePreviewParam::class) state: RoomDirectoryState) = ElementPreview { RoomDirectoryView( state = state, onResultClick = {}, diff --git a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenterTest.kt b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenterTest.kt index 6295e4beb6..7d75703587 100644 --- a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenterTest.kt +++ b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryPresenterTest.kt @@ -91,7 +91,7 @@ class RoomDirectoryPresenterTest { val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService) presenter.test { awaitItem().also { state -> - state.eventSink(RoomDirectoryEvents.Search("test")) + state.eventSink(RoomDirectoryEvent.Search("test")) } awaitItem().also { state -> assertThat(state.query).isEqualTo("test") @@ -112,7 +112,7 @@ class RoomDirectoryPresenterTest { val presenter = createRoomDirectoryPresenter(roomDirectoryService = roomDirectoryService) presenter.test { awaitItem().also { state -> - state.eventSink(RoomDirectoryEvents.LoadMore) + state.eventSink(RoomDirectoryEvent.LoadMore) } advanceUntilIdle() cancelAndIgnoreRemainingEvents() diff --git a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt index b3b935596f..1c26290f10 100644 --- a/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt +++ b/features/roomdirectory/impl/src/test/kotlin/io/element/android/features/roomdirectory/impl/root/RoomDirectoryViewTest.kt @@ -30,7 +30,7 @@ import org.junit.Test class RoomDirectoryViewTest : RobolectricTest() { @Test fun `typing text in search field emits the expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setRoomDirectoryView( state = aRoomDirectoryState( eventSink = eventsRecorder, @@ -39,12 +39,12 @@ class RoomDirectoryViewTest : RobolectricTest() { onNodeWithTag(TestTags.searchTextField.value).performTextInput( text = "Test" ) - eventsRecorder.assertSingle(RoomDirectoryEvents.Search("Test")) + eventsRecorder.assertSingle(RoomDirectoryEvent.Search("Test")) } @Test fun `clicking on room item then onResultClick lambda is called once`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() val state = aRoomDirectoryState( roomDescriptions = aRoomDescriptionList(), eventSink = eventsRecorder, @@ -61,13 +61,13 @@ class RoomDirectoryViewTest : RobolectricTest() { @Test fun `composing load more indicator emits expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() val state = aRoomDirectoryState( displayLoadMoreIndicator = true, eventSink = eventsRecorder, ) setRoomDirectoryView(state = state) - eventsRecorder.assertSingle(RoomDirectoryEvents.LoadMore) + eventsRecorder.assertSingle(RoomDirectoryEvent.LoadMore) } } diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvent.kt similarity index 84% rename from features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt rename to features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvent.kt index d452ff4e66..403268a18d 100644 --- a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvents.kt +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationEvent.kt @@ -13,7 +13,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser /** * Events sent to the moderation presenter through [RoomMemberModerationState.eventSink]. */ -interface RoomMemberModerationEvents { - data class ShowActionsForUser(val user: MatrixUser) : RoomMemberModerationEvents - data class ProcessAction(val action: ModerationAction, val targetUser: MatrixUser) : RoomMemberModerationEvents +interface RoomMemberModerationEvent { + data class ShowActionsForUser(val user: MatrixUser) : RoomMemberModerationEvent + data class ProcessAction(val action: ModerationAction, val targetUser: MatrixUser) : RoomMemberModerationEvent } diff --git a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt index 9227c77edf..8c73aaee0e 100644 --- a/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt +++ b/features/roommembermoderation/api/src/main/kotlin/io/element/android/features/roommembermoderation/api/RoomMemberModerationState.kt @@ -19,7 +19,7 @@ interface RoomMemberModerationState { val permissions: RoomMemberModerationPermissions /** Where the host screen sends its events to open the actions of a member or run one of them. */ - val eventSink: (RoomMemberModerationEvents) -> Unit + val eventSink: (RoomMemberModerationEvent) -> Unit } data class ModerationActionState( diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationEvents.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationEvent.kt similarity index 71% rename from features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationEvents.kt rename to features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationEvent.kt index 2bc76db761..741cb42961 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationEvents.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationEvent.kt @@ -8,11 +8,11 @@ package io.element.android.features.roommembermoderation.impl -import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent -sealed interface InternalRoomMemberModerationEvents : RoomMemberModerationEvents { - data class DoKickUser(val reason: String) : InternalRoomMemberModerationEvents - data class DoBanUser(val reason: String) : InternalRoomMemberModerationEvents - data class DoUnbanUser(val reason: String) : InternalRoomMemberModerationEvents - data object Reset : InternalRoomMemberModerationEvents +sealed interface InternalRoomMemberModerationEvent : RoomMemberModerationEvent { + data class DoKickUser(val reason: String) : InternalRoomMemberModerationEvent + data class DoBanUser(val reason: String) : InternalRoomMemberModerationEvent + data class DoUnbanUser(val reason: String) : InternalRoomMemberModerationEvent + data object Reset : InternalRoomMemberModerationEvent } diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt index f45a277992..adb9e84e1d 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationState.kt @@ -9,7 +9,7 @@ package io.element.android.features.roommembermoderation.impl import io.element.android.features.roommembermoderation.api.ModerationActionState -import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncAction @@ -23,7 +23,7 @@ data class InternalRoomMemberModerationState( val kickUserAsyncAction: AsyncAction, val banUserAsyncAction: AsyncAction, val unbanUserAsyncAction: AsyncAction, - override val eventSink: (RoomMemberModerationEvents) -> Unit, + override val eventSink: (RoomMemberModerationEvent) -> Unit, ) : RoomMemberModerationState { val canDisplayActions = actions.isNotEmpty() } diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStatePreviewParam.kt similarity index 95% rename from features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt rename to features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStatePreviewParam.kt index 2c32f2c47a..1da2392737 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStateProvider.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/InternalRoomMemberModerationStatePreviewParam.kt @@ -11,7 +11,7 @@ package io.element.android.features.roommembermoderation.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.ModerationActionState -import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE @@ -20,7 +20,7 @@ import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.toImmutableList -class InternalRoomMemberModerationStateProvider : PreviewParameterProvider { +class InternalRoomMemberModerationStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomMembersModerationState( @@ -93,7 +93,7 @@ fun aRoomMembersModerationState( kickUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, banUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, unbanUserAsyncAction: AsyncAction = AsyncAction.Uninitialized, - eventSink: (RoomMemberModerationEvents) -> Unit = {}, + eventSink: (RoomMemberModerationEvent) -> Unit = {}, ) = InternalRoomMemberModerationState( permissions = permissions, selectedUser = selectedUser, diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt index 2a876fc484..49d7ced4d7 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenter.kt @@ -20,7 +20,7 @@ import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.RoomModeration import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.ModerationActionState -import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.features.roommembermoderation.api.roomMemberModerationPermissions @@ -77,9 +77,9 @@ class RoomMemberModerationPresenter( } val moderationActions = remember { mutableStateOf>(persistentListOf()) } - fun handleEvent(event: RoomMemberModerationEvents) { + fun handleEvent(event: RoomMemberModerationEvent) { when (event) { - is RoomMemberModerationEvents.ShowActionsForUser -> { + is RoomMemberModerationEvent.ShowActionsForUser -> { selectedUser = event.user val member = room.membersStateFlow.value.roomMembers()?.firstOrNull { it.userId == event.user.userId @@ -90,7 +90,7 @@ class RoomMemberModerationPresenter( currentUserPowerLevel = currentUserPowerLevel, ) } - is RoomMemberModerationEvents.ProcessAction -> { + is RoomMemberModerationEvent.ProcessAction -> { // First, hide any list of existing actions that could be displayed moderationActions.value = persistentListOf() @@ -110,25 +110,25 @@ class RoomMemberModerationPresenter( } } } - is InternalRoomMemberModerationEvents.DoKickUser -> { + is InternalRoomMemberModerationEvent.DoKickUser -> { selectedUser?.let { coroutineScope.kickUser(it.userId, event.reason, kickUserAsyncAction) } selectedUser = null } - is InternalRoomMemberModerationEvents.DoBanUser -> { + is InternalRoomMemberModerationEvent.DoBanUser -> { selectedUser?.let { coroutineScope.banUser(it.userId, event.reason, banUserAsyncAction) } selectedUser = null } - is InternalRoomMemberModerationEvents.DoUnbanUser -> { + is InternalRoomMemberModerationEvent.DoUnbanUser -> { selectedUser?.let { coroutineScope.unbanUser(it.userId, event.reason, unbanUserAsyncAction) } selectedUser = null } - is InternalRoomMemberModerationEvents.Reset -> { + is InternalRoomMemberModerationEvent.Reset -> { selectedUser = null moderationActions.value = persistentListOf() kickUserAsyncAction.value = AsyncAction.Uninitialized diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt index a5e0ac274f..3ec00fd232 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationView.kt @@ -77,7 +77,7 @@ fun RoomMemberModerationView( actions = state.actions, onSelectAction = onSelectAction, onAvatarClick = onAvatarClick, - onDismiss = { state.eventSink(InternalRoomMemberModerationEvents.Reset) }, + onDismiss = { state.eventSink(InternalRoomMemberModerationEvent.Reset) }, ) } RoomMemberAsyncActions(state = state) @@ -102,9 +102,9 @@ private fun RoomMemberAsyncActions( destructiveSubmit = true, minLines = 2, onSubmit = { reason -> - state.eventSink(InternalRoomMemberModerationEvents.DoKickUser(reason = reason)) + state.eventSink(InternalRoomMemberModerationEvent.DoKickUser(reason = reason)) }, - onDismissRequest = { state.eventSink(InternalRoomMemberModerationEvents.Reset) }, + onDismissRequest = { state.eventSink(InternalRoomMemberModerationEvent.Reset) }, placeholder = stringResource(id = CommonStrings.common_reason), content = stringResource(R.string.screen_bottom_sheet_manage_room_member_kick_member_confirmation_description), value = "", @@ -142,9 +142,9 @@ private fun RoomMemberAsyncActions( destructiveSubmit = true, minLines = 2, onSubmit = { reason -> - state.eventSink(InternalRoomMemberModerationEvents.DoBanUser(reason = reason)) + state.eventSink(InternalRoomMemberModerationEvent.DoBanUser(reason = reason)) }, - onDismissRequest = { state.eventSink(InternalRoomMemberModerationEvents.Reset) }, + onDismissRequest = { state.eventSink(InternalRoomMemberModerationEvent.Reset) }, placeholder = stringResource(id = CommonStrings.common_reason), content = stringResource(R.string.screen_bottom_sheet_manage_room_member_ban_member_confirmation_description), value = "", @@ -185,9 +185,9 @@ private fun RoomMemberAsyncActions( asyncIndicatorState.enqueue { AsyncIndicator.Loading(text = stringResource(R.string.screen_bottom_sheet_manage_room_member_unbanning_user, userDisplayName)) } - state.eventSink(InternalRoomMemberModerationEvents.DoUnbanUser(reason = reason)) + state.eventSink(InternalRoomMemberModerationEvent.DoUnbanUser(reason = reason)) }, - onDismissRequest = { state.eventSink(InternalRoomMemberModerationEvents.Reset) }, + onDismissRequest = { state.eventSink(InternalRoomMemberModerationEvent.Reset) }, placeholder = stringResource(id = CommonStrings.common_reason), content = stringResource(R.string.screen_bottom_sheet_manage_room_member_unban_member_confirmation_description), value = "", @@ -362,7 +362,7 @@ private fun RoomMemberActionsBottomSheet( @PreviewsDayNight @Composable -internal fun RoomMemberModerationViewPreview(@PreviewParameter(InternalRoomMemberModerationStateProvider::class) state: InternalRoomMemberModerationState) { +internal fun RoomMemberModerationViewPreview(@PreviewParameter(InternalRoomMemberModerationStatePreviewParam::class) state: InternalRoomMemberModerationState) { val isDoingAction = listOf(state.kickUserAsyncAction, state.banUserAsyncAction, state.unbanUserAsyncAction).any { it is AsyncAction.Loading } val modifier = if (isDoingAction) { Modifier.fillMaxWidth().heightIn(min = 64.dp) diff --git a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/di/RoomMemberModerationModule.kt b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/di/RoomMemberModerationBindingContainer.kt similarity index 94% rename from features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/di/RoomMemberModerationModule.kt rename to features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/di/RoomMemberModerationBindingContainer.kt index ab82f6ced4..1ffe6df41f 100644 --- a/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/di/RoomMemberModerationModule.kt +++ b/features/roommembermoderation/impl/src/main/kotlin/io/element/android/features/roommembermoderation/impl/di/RoomMemberModerationBindingContainer.kt @@ -18,7 +18,7 @@ import io.element.android.libraries.di.RoomScope @ContributesTo(RoomScope::class) @BindingContainer -interface RoomMemberModerationModule { +interface RoomMemberModerationBindingContainer { @Binds fun bindRoomMemberModerationPresenter(presenter: RoomMemberModerationPresenter): Presenter } diff --git a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt index 3397f3a40a..1f13b7e68f 100644 --- a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt +++ b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationPresenterTest.kt @@ -12,7 +12,7 @@ import app.cash.turbine.TurbineTestContext import com.google.common.truth.Truth.assertThat import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.ModerationActionState -import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent import io.element.android.features.roommembermoderation.api.RoomMemberModerationPermissions import io.element.android.features.roommembermoderation.api.RoomMemberModerationState import io.element.android.libraries.architecture.AsyncAction @@ -73,7 +73,7 @@ class RoomMemberModerationPresenterTest { ) createRoomMemberModerationPresenter(room = room).test { val initialState = awaitState() - initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser)) + initialState.eventSink(RoomMemberModerationEvent.ShowActionsForUser(targetUser)) skipItems(1) val updatedState = awaitState() assertThat(updatedState.selectedUser).isEqualTo(targetUser) @@ -93,7 +93,7 @@ class RoomMemberModerationPresenterTest { ) createRoomMemberModerationPresenter(room = room).test { val initialState = awaitState() - initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser)) + initialState.eventSink(RoomMemberModerationEvent.ShowActionsForUser(targetUser)) skipItems(2) val updatedState = awaitState() assertThat(updatedState.selectedUser).isEqualTo(targetUser) @@ -115,7 +115,7 @@ class RoomMemberModerationPresenterTest { ) createRoomMemberModerationPresenter(room = room).test { val initialState = awaitState() - initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser)) + initialState.eventSink(RoomMemberModerationEvent.ShowActionsForUser(targetUser)) skipItems(2) val updatedState = awaitState() assertThat(updatedState.selectedUser).isEqualTo(targetUser) @@ -137,7 +137,7 @@ class RoomMemberModerationPresenterTest { ) createRoomMemberModerationPresenter(room = room).test { val initialState = awaitState() - initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser)) + initialState.eventSink(RoomMemberModerationEvent.ShowActionsForUser(targetUser)) skipItems(2) val updatedState = awaitState() assertThat(updatedState.selectedUser).isEqualTo(targetUser) @@ -159,7 +159,7 @@ class RoomMemberModerationPresenterTest { ) createRoomMemberModerationPresenter(room = room).test { val initialState = awaitState() - initialState.eventSink(RoomMemberModerationEvents.ShowActionsForUser(targetUser)) + initialState.eventSink(RoomMemberModerationEvent.ShowActionsForUser(targetUser)) skipItems(2) val updatedState = awaitState() assertThat(updatedState.selectedUser).isEqualTo(targetUser) @@ -175,7 +175,7 @@ class RoomMemberModerationPresenterTest { createRoomMemberModerationPresenter(room = aJoinedRoom()).test { val initialState = awaitState() initialState.eventSink( - RoomMemberModerationEvents.ProcessAction( + RoomMemberModerationEvent.ProcessAction( targetUser = targetUser, action = ModerationAction.KickUser ) @@ -192,7 +192,7 @@ class RoomMemberModerationPresenterTest { createRoomMemberModerationPresenter(room = aJoinedRoom()).test { val initialState = awaitState() initialState.eventSink( - RoomMemberModerationEvents.ProcessAction( + RoomMemberModerationEvent.ProcessAction( targetUser = targetUser, action = ModerationAction.BanUser ) @@ -209,7 +209,7 @@ class RoomMemberModerationPresenterTest { createRoomMemberModerationPresenter(room = aJoinedRoom()).test { val initialState = awaitState() initialState.eventSink( - RoomMemberModerationEvents.ProcessAction( + RoomMemberModerationEvent.ProcessAction( targetUser = targetUser, action = ModerationAction.UnbanUser ) @@ -235,13 +235,13 @@ class RoomMemberModerationPresenterTest { createRoomMemberModerationPresenter(room = room).test { val initialState = awaitState() initialState.eventSink( - RoomMemberModerationEvents.ProcessAction( + RoomMemberModerationEvent.ProcessAction( targetUser = targetUser, action = ModerationAction.KickUser ) ) skipItems(2) - initialState.eventSink(InternalRoomMemberModerationEvents.DoKickUser("Reason")) + initialState.eventSink(InternalRoomMemberModerationEvent.DoKickUser("Reason")) skipItems(1) val loadingState = awaitState() assertThat(loadingState.kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) @@ -265,13 +265,13 @@ class RoomMemberModerationPresenterTest { createRoomMemberModerationPresenter(room = room).test { val initialState = awaitState() initialState.eventSink( - RoomMemberModerationEvents.ProcessAction( + RoomMemberModerationEvent.ProcessAction( targetUser = targetUser, action = ModerationAction.BanUser ) ) skipItems(2) - initialState.eventSink(InternalRoomMemberModerationEvents.DoBanUser("Reason")) + initialState.eventSink(InternalRoomMemberModerationEvent.DoBanUser("Reason")) skipItems(1) val loadingState = awaitState() assertThat(loadingState.banUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) @@ -295,13 +295,13 @@ class RoomMemberModerationPresenterTest { createRoomMemberModerationPresenter(room = room).test { val initialState = awaitState() initialState.eventSink( - RoomMemberModerationEvents.ProcessAction( + RoomMemberModerationEvent.ProcessAction( targetUser = targetUser, action = ModerationAction.UnbanUser ) ) skipItems(2) - initialState.eventSink(InternalRoomMemberModerationEvents.DoUnbanUser("Reason")) + initialState.eventSink(InternalRoomMemberModerationEvent.DoUnbanUser("Reason")) skipItems(1) val loadingState = awaitState() assertThat(loadingState.unbanUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) @@ -320,13 +320,13 @@ class RoomMemberModerationPresenterTest { createRoomMemberModerationPresenter(room = room).test { val initialState = awaitState() initialState.eventSink( - RoomMemberModerationEvents.ProcessAction( + RoomMemberModerationEvent.ProcessAction( targetUser = targetUser, action = ModerationAction.KickUser ) ) skipItems(2) - initialState.eventSink(InternalRoomMemberModerationEvents.DoKickUser("Reason")) + initialState.eventSink(InternalRoomMemberModerationEvent.DoKickUser("Reason")) skipItems(1) val loadingState = awaitState() assertThat(loadingState.kickUserAsyncAction).isInstanceOf(AsyncAction.Loading::class.java) @@ -340,10 +340,10 @@ class RoomMemberModerationPresenterTest { createRoomMemberModerationPresenter(room = aJoinedRoom()).test { val initialState = awaitState() initialState.eventSink( - RoomMemberModerationEvents.ProcessAction(targetUser = targetUser, action = ModerationAction.BanUser) + RoomMemberModerationEvent.ProcessAction(targetUser = targetUser, action = ModerationAction.BanUser) ) skipItems(2) - initialState.eventSink(InternalRoomMemberModerationEvents.Reset) + initialState.eventSink(InternalRoomMemberModerationEvent.Reset) skipItems(1) val resetState = awaitState() assertThat(resetState.selectedUser).isNull() diff --git a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt index f99e517767..ff7183ed5f 100644 --- a/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt +++ b/features/roommembermoderation/impl/src/test/kotlin/io/element/android/features/roommembermoderation/impl/RoomMemberModerationViewTest.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.roommembermoderation.api.ModerationAction import io.element.android.features.roommembermoderation.api.ModerationActionState -import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvents +import io.element.android.features.roommembermoderation.api.RoomMemberModerationEvent import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.testtags.TestTags @@ -34,7 +34,7 @@ class RoomMemberModerationViewTest : RobolectricTest() { @Test fun `clicking on display profile action calls onSelectAction`() = runAndroidComposeUiTest { val user = anAlice() - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithTwoParams(ModerationAction.DisplayProfile, user) { callback -> setRoomMemberModerationView( aRoomMembersModerationState( @@ -55,7 +55,7 @@ class RoomMemberModerationViewTest : RobolectricTest() { @Test fun `clicking on kick user action calls onSelectAction`() = runAndroidComposeUiTest { val user = anAlice() - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithTwoParams(ModerationAction.KickUser, user) { callback -> setRoomMemberModerationView( aRoomMembersModerationState( @@ -76,7 +76,7 @@ class RoomMemberModerationViewTest : RobolectricTest() { @Test fun `clicking on ban user action calls onSelectAction`() = runAndroidComposeUiTest { val user = anAlice() - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithTwoParams(ModerationAction.BanUser, user) { callback -> setRoomMemberModerationView( aRoomMembersModerationState( @@ -97,7 +97,7 @@ class RoomMemberModerationViewTest : RobolectricTest() { @Test fun `clicking on unban user action calls onSelectAction`() = runAndroidComposeUiTest { val user = anAlice() - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithTwoParams(ModerationAction.UnbanUser, user) { callback -> setRoomMemberModerationView( aRoomMembersModerationState( @@ -117,7 +117,7 @@ class RoomMemberModerationViewTest : RobolectricTest() { @Test fun `clicking submit on kick confirmation dialog sends DoKickUser event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), @@ -126,12 +126,12 @@ class RoomMemberModerationViewTest : RobolectricTest() { ), ) pressTag(TestTags.dialogPositive.value) - eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoKickUser(reason = "")) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvent.DoKickUser(reason = "")) } @Test fun `clicking dismiss on kick confirmation dialog sends Reset event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), @@ -140,12 +140,12 @@ class RoomMemberModerationViewTest : RobolectricTest() { ), ) pressTag(TestTags.dialogNegative.value) - eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvent.Reset) } @Test fun `clicking submit on ban confirmation dialog sends DoBanUser event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), @@ -154,12 +154,12 @@ class RoomMemberModerationViewTest : RobolectricTest() { ), ) pressTag(TestTags.dialogPositive.value) - eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoBanUser(reason = "")) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvent.DoBanUser(reason = "")) } @Test fun `clicking dismiss on ban confirmation dialog sends Reset event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), @@ -168,12 +168,12 @@ class RoomMemberModerationViewTest : RobolectricTest() { ), ) pressTag(TestTags.dialogNegative.value) - eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvent.Reset) } @Test fun `clicking confirm on unban confirmation dialog sends DoUnbanUser event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), @@ -182,12 +182,12 @@ class RoomMemberModerationViewTest : RobolectricTest() { ), ) pressTag(TestTags.dialogPositive.value) - eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.DoUnbanUser("")) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvent.DoUnbanUser("")) } @Test fun `clicking dismiss on unban confirmation dialog sends Reset event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), @@ -196,12 +196,12 @@ class RoomMemberModerationViewTest : RobolectricTest() { ), ) pressTag(TestTags.dialogNegative.value) - eventsRecorder.assertSingle(InternalRoomMemberModerationEvents.Reset) + eventsRecorder.assertSingle(InternalRoomMemberModerationEvent.Reset) } @Test fun `disabled actions are not clickable`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) setRoomMemberModerationView( aRoomMembersModerationState( selectedUser = anAlice(), diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableEvents.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableEvent.kt similarity index 65% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableEvents.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableEvent.kt index 05d05a4840..330faa47f5 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableEvents.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableEvent.kt @@ -8,7 +8,7 @@ package io.element.android.features.securebackup.impl.disable -sealed interface SecureBackupDisableEvents { - data object DisableBackup : SecureBackupDisableEvents - data object DismissDialogs : SecureBackupDisableEvents +sealed interface SecureBackupDisableEvent { + data object DisableBackup : SecureBackupDisableEvent + data object DismissDialogs : SecureBackupDisableEvent } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenter.kt index 67b6816097..13a2882fa8 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenter.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenter.kt @@ -37,10 +37,10 @@ class SecureBackupDisablePresenter( Timber.tag(loggerTagDisable.value).d("backupState: $backupState") val disableAction: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } val coroutineScope = rememberCoroutineScope() - fun handleEvent(event: SecureBackupDisableEvents) { + fun handleEvent(event: SecureBackupDisableEvent) { when (event) { - is SecureBackupDisableEvents.DisableBackup -> coroutineScope.disableBackup(disableAction) - SecureBackupDisableEvents.DismissDialogs -> { + is SecureBackupDisableEvent.DisableBackup -> coroutineScope.disableBackup(disableAction) + SecureBackupDisableEvent.DismissDialogs -> { disableAction.value = AsyncAction.Uninitialized } } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableState.kt index a4f6b210ab..1e1b1ec35e 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableState.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableState.kt @@ -15,5 +15,5 @@ data class SecureBackupDisableState( val backupState: BackupState, val disableAction: AsyncAction, val appName: String, - val eventSink: (SecureBackupDisableEvents) -> Unit + val eventSink: (SecureBackupDisableEvent) -> Unit ) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableStatePreviewParam.kt similarity index 92% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableStateProvider.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableStatePreviewParam.kt index 6aa10d9a0b..6eea9c366a 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableStateProvider.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableStatePreviewParam.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.encryption.BackupState -open class SecureBackupDisableStateProvider : PreviewParameterProvider { +open class SecureBackupDisableStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aSecureBackupDisableState(), diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt index 24678148b4..10019caae6 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisableView.kt @@ -55,7 +55,7 @@ fun SecureBackupDisableView( async = state.disableAction, progressDialog = {}, errorMessage = { it.message ?: it.toString() }, - onErrorDismiss = { state.eventSink.invoke(SecureBackupDisableEvents.DismissDialogs) }, + onErrorDismiss = { state.eventSink.invoke(SecureBackupDisableEvent.DismissDialogs) }, onSuccess = { onSuccess() }, ) } @@ -69,7 +69,7 @@ private fun ColumnScope.Buttons( showProgress = state.disableAction.isLoading(), destructive = true, modifier = Modifier.fillMaxWidth(), - onClick = { state.eventSink.invoke(SecureBackupDisableEvents.DisableBackup) } + onClick = { state.eventSink.invoke(SecureBackupDisableEvent.DisableBackup) } ) } @@ -112,7 +112,7 @@ private fun SecureBackupDisableItem(text: String) { @PreviewsDayNight @Composable internal fun SecureBackupDisableViewPreview( - @PreviewParameter(SecureBackupDisableStateProvider::class) state: SecureBackupDisableState + @PreviewParameter(SecureBackupDisableStatePreviewParam::class) state: SecureBackupDisableState ) = ElementPreview { SecureBackupDisableView( state = state, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyEvents.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyEvent.kt similarity index 62% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyEvents.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyEvent.kt index 644756b957..ac5aa751b6 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyEvents.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyEvent.kt @@ -8,9 +8,9 @@ package io.element.android.features.securebackup.impl.enter -sealed interface SecureBackupEnterRecoveryKeyEvents { - data class OnRecoveryKeyChange(val recoveryKey: String) : SecureBackupEnterRecoveryKeyEvents - data class ChangeRecoveryKeyFieldContentsVisibility(val visible: Boolean) : SecureBackupEnterRecoveryKeyEvents - data object Submit : SecureBackupEnterRecoveryKeyEvents - data object ClearDialog : SecureBackupEnterRecoveryKeyEvents +sealed interface SecureBackupEnterRecoveryKeyEvent { + data class OnRecoveryKeyChange(val recoveryKey: String) : SecureBackupEnterRecoveryKeyEvent + data class ChangeRecoveryKeyFieldContentsVisibility(val visible: Boolean) : SecureBackupEnterRecoveryKeyEvent + data object Submit : SecureBackupEnterRecoveryKeyEvent + data object ClearDialog : SecureBackupEnterRecoveryKeyEvent } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenter.kt index 9a71cf1727..bbc61cc283 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenter.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenter.kt @@ -45,12 +45,12 @@ class SecureBackupEnterRecoveryKeyPresenter( mutableStateOf(AsyncAction.Uninitialized) } - fun handleEvent(event: SecureBackupEnterRecoveryKeyEvents) { + fun handleEvent(event: SecureBackupEnterRecoveryKeyEvent) { when (event) { - SecureBackupEnterRecoveryKeyEvents.ClearDialog -> { + SecureBackupEnterRecoveryKeyEvent.ClearDialog -> { submitAction.value = AsyncAction.Uninitialized } - is SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange -> { + is SecureBackupEnterRecoveryKeyEvent.OnRecoveryKeyChange -> { val previousRecoveryKey = recoveryKey recoveryKey = if (previousRecoveryKey.isEmpty() && recoveryKeyTools.isRecoveryKeyFormatValid(event.recoveryKey)) { // A Recovery key has been entered, remove the spaces for a better rendering @@ -60,11 +60,11 @@ class SecureBackupEnterRecoveryKeyPresenter( event.recoveryKey } } - SecureBackupEnterRecoveryKeyEvents.Submit -> { + SecureBackupEnterRecoveryKeyEvent.Submit -> { // No need to remove the spaces, the SDK will do it. coroutineScope.submitRecoveryKey(recoveryKey, submitAction) } - is SecureBackupEnterRecoveryKeyEvents.ChangeRecoveryKeyFieldContentsVisibility -> { + is SecureBackupEnterRecoveryKeyEvent.ChangeRecoveryKeyFieldContentsVisibility -> { displayRecoveryKeyFieldContents = event.visible } } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyState.kt index 45a6989c15..188419553a 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyState.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyState.kt @@ -15,5 +15,5 @@ data class SecureBackupEnterRecoveryKeyState( val recoveryKeyViewState: RecoveryKeyViewState, val isSubmitEnabled: Boolean, val submitAction: AsyncAction, - val eventSink: (SecureBackupEnterRecoveryKeyEvents) -> Unit + val eventSink: (SecureBackupEnterRecoveryKeyEvent) -> Unit ) diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyStatePreviewParam.kt similarity index 90% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyStateProvider.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyStatePreviewParam.kt index 5e95b5713e..90a040106c 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyStateProvider.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyStatePreviewParam.kt @@ -14,7 +14,7 @@ import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyView import io.element.android.features.securebackup.impl.setup.views.aFormattedRecoveryKey import io.element.android.libraries.architecture.AsyncAction -open class SecureBackupEnterRecoveryKeyStateProvider : PreviewParameterProvider { +open class SecureBackupEnterRecoveryKeyStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aSecureBackupEnterRecoveryKeyState(recoveryKey = ""), @@ -30,7 +30,7 @@ fun aSecureBackupEnterRecoveryKeyState( isSubmitEnabled: Boolean = recoveryKey.isNotEmpty(), displayTextFieldContents: Boolean = true, submitAction: AsyncAction = AsyncAction.Uninitialized, - eventSink: (SecureBackupEnterRecoveryKeyEvents) -> Unit = {}, + eventSink: (SecureBackupEnterRecoveryKeyEvent) -> Unit = {}, ) = SecureBackupEnterRecoveryKeyState( recoveryKeyViewState = RecoveryKeyViewState( recoveryKeyUserStory = RecoveryKeyUserStory.Enter, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt index 34fca33e96..db3f228f8b 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyView.kt @@ -41,7 +41,7 @@ fun SecureBackupEnterRecoveryKeyView( progressDialog = { }, errorTitle = { stringResource(id = R.string.screen_recovery_key_confirm_error_title) }, errorMessage = { stringResource(id = R.string.screen_recovery_key_confirm_error_content) }, - onErrorDismiss = { state.eventSink(SecureBackupEnterRecoveryKeyEvents.ClearDialog) }, + onErrorDismiss = { state.eventSink(SecureBackupEnterRecoveryKeyEvent.ClearDialog) }, ) FlowStepPage( @@ -68,13 +68,13 @@ private fun Content( state = state.recoveryKeyViewState, onClick = null, onChange = { - state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange(it)) + state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvent.OnRecoveryKeyChange(it)) }, onSubmit = { - state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit) + state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvent.Submit) }, toggleRecoveryKeyVisibility = { - state.eventSink(SecureBackupEnterRecoveryKeyEvents.ChangeRecoveryKeyFieldContentsVisibility(it)) + state.eventSink(SecureBackupEnterRecoveryKeyEvent.ChangeRecoveryKeyFieldContentsVisibility(it)) } ) } @@ -89,7 +89,7 @@ private fun ColumnScope.Buttons( showProgress = state.submitAction.isLoading(), modifier = Modifier.fillMaxWidth(), onClick = { - state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvents.Submit) + state.eventSink.invoke(SecureBackupEnterRecoveryKeyEvent.Submit) } ) } @@ -97,7 +97,7 @@ private fun ColumnScope.Buttons( @PreviewsDayNight @Composable internal fun SecureBackupEnterRecoveryKeyViewPreview( - @PreviewParameter(SecureBackupEnterRecoveryKeyStateProvider::class) state: SecureBackupEnterRecoveryKeyState + @PreviewParameter(SecureBackupEnterRecoveryKeyStatePreviewParam::class) state: SecureBackupEnterRecoveryKeyState ) = ElementPreview { SecureBackupEnterRecoveryKeyView( state = state, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordStatePreviewParam.kt similarity index 91% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordStateProvider.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordStatePreviewParam.kt index fc4ce2430e..b6e786a9bc 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordStateProvider.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordStatePreviewParam.kt @@ -11,7 +11,7 @@ package io.element.android.features.securebackup.impl.reset.password import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction -class ResetIdentityPasswordStateProvider : PreviewParameterProvider { +class ResetIdentityPasswordStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aResetIdentityPasswordState(), diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt index 3bb77ac206..27612a7b4d 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/password/ResetIdentityPasswordView.kt @@ -106,7 +106,7 @@ private fun Content(text: String, onTextChange: (String) -> Unit, hasError: Bool @PreviewsDayNight @Composable -internal fun ResetIdentityPasswordViewPreview(@PreviewParameter(ResetIdentityPasswordStateProvider::class) state: ResetIdentityPasswordState) { +internal fun ResetIdentityPasswordViewPreview(@PreviewParameter(ResetIdentityPasswordStatePreviewParam::class) state: ResetIdentityPasswordState) { ElementPreview { ResetIdentityPasswordView( state = state, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootStatePreviewParam.kt similarity index 88% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootStateProvider.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootStatePreviewParam.kt index 697401367f..e4bb24355e 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootStateProvider.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootStatePreviewParam.kt @@ -10,7 +10,7 @@ package io.element.android.features.securebackup.impl.reset.root import androidx.compose.ui.tooling.preview.PreviewParameterProvider -class ResetIdentityRootStateProvider : PreviewParameterProvider { +class ResetIdentityRootStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( ResetIdentityRootState( diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt index 7ac8dbe4cf..0109bd4faf 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/reset/root/ResetIdentityRootView.kt @@ -130,7 +130,7 @@ private fun Content() { @PreviewsDayNight @Composable -internal fun ResetIdentityRootViewPreview(@PreviewParameter(ResetIdentityRootStateProvider::class) state: ResetIdentityRootState) { +internal fun ResetIdentityRootViewPreview(@PreviewParameter(ResetIdentityRootStatePreviewParam::class) state: ResetIdentityRootState) { ElementPreview { ResetIdentityRootView( state = state, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootEvents.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootEvent.kt similarity index 62% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootEvents.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootEvent.kt index 9d371e3c30..91bcf67575 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootEvents.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootEvent.kt @@ -8,9 +8,9 @@ package io.element.android.features.securebackup.impl.root -sealed interface SecureBackupRootEvents { - data object RetryKeyBackupState : SecureBackupRootEvents - data object EnableKeyStorage : SecureBackupRootEvents - data object DisplayKeyStorageDisabledError : SecureBackupRootEvents - data object DismissDialog : SecureBackupRootEvents +sealed interface SecureBackupRootEvent { + data object RetryKeyBackupState : SecureBackupRootEvent + data object EnableKeyStorage : SecureBackupRootEvent + data object DisplayKeyStorageDisabledError : SecureBackupRootEvent + data object DismissDialog : SecureBackupRootEvent } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenter.kt index 9a97bfac25..602fd773da 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenter.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenter.kt @@ -59,15 +59,15 @@ class SecureBackupRootPresenter( } } - fun handleEvent(event: SecureBackupRootEvents) { + fun handleEvent(event: SecureBackupRootEvent) { when (event) { - SecureBackupRootEvents.RetryKeyBackupState -> localCoroutineScope.getKeyBackupStatus(doesBackupExistOnServerAction) - SecureBackupRootEvents.EnableKeyStorage -> localCoroutineScope.enableBackup(enableAction) - SecureBackupRootEvents.DismissDialog -> { + SecureBackupRootEvent.RetryKeyBackupState -> localCoroutineScope.getKeyBackupStatus(doesBackupExistOnServerAction) + SecureBackupRootEvent.EnableKeyStorage -> localCoroutineScope.enableBackup(enableAction) + SecureBackupRootEvent.DismissDialog -> { enableAction.value = AsyncAction.Uninitialized displayKeyStorageDisabledError = false } - SecureBackupRootEvents.DisplayKeyStorageDisabledError -> displayKeyStorageDisabledError = true + SecureBackupRootEvent.DisplayKeyStorageDisabledError -> displayKeyStorageDisabledError = true } } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootState.kt index 1cbf917ca5..f528910fe2 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootState.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootState.kt @@ -22,7 +22,7 @@ data class SecureBackupRootState( val appName: String, val displayKeyStorageDisabledError: Boolean, val snackbarMessage: SnackbarMessage?, - val eventSink: (SecureBackupRootEvents) -> Unit, + val eventSink: (SecureBackupRootEvent) -> Unit, ) { val isKeyStorageEnabled: Boolean get() = when (backupState) { diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStatePreviewParam.kt similarity index 97% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStateProvider.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStatePreviewParam.kt index 5dc3bea7e8..3247db78eb 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStateProvider.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootStatePreviewParam.kt @@ -15,7 +15,7 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.encryption.BackupState import io.element.android.libraries.matrix.api.encryption.RecoveryState -open class SecureBackupRootStateProvider : PreviewParameterProvider { +open class SecureBackupRootStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aSecureBackupRootState(backupState = BackupState.UNKNOWN, doesBackupExistOnServer = AsyncData.Uninitialized), diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootView.kt index 626a162152..fb45228ce4 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootView.kt @@ -114,12 +114,12 @@ fun SecureBackupRootView( if (state.doesBackupExistOnServer.data) { onDisableClick() } else { - state.eventSink.invoke(SecureBackupRootEvents.EnableKeyStorage) + state.eventSink.invoke(SecureBackupRootEvent.EnableKeyStorage) } } is AsyncData.Loading, AsyncData.Uninitialized -> Unit - is AsyncData.Failure -> state.eventSink.invoke(SecureBackupRootEvents.RetryKeyBackupState) + is AsyncData.Failure -> state.eventSink.invoke(SecureBackupRootEvent.RetryKeyBackupState) } } BackupState.CREATING, @@ -154,7 +154,7 @@ fun SecureBackupRootView( if (state.isKeyStorageEnabled) { onSetupClick() } else { - state.eventSink.invoke(SecureBackupRootEvents.DisplayKeyStorageDisabledError) + state.eventSink.invoke(SecureBackupRootEvent.DisplayKeyStorageDisabledError) } }, ) @@ -177,7 +177,7 @@ fun SecureBackupRootView( if (state.isKeyStorageEnabled) { onChangeClick() } else { - state.eventSink.invoke(SecureBackupRootEvents.DisplayKeyStorageDisabledError) + state.eventSink.invoke(SecureBackupRootEvent.DisplayKeyStorageDisabledError) } }, ) @@ -201,7 +201,7 @@ fun SecureBackupRootView( if (state.isKeyStorageEnabled) { onConfirmRecoveryKeyClick() } else { - state.eventSink.invoke(SecureBackupRootEvents.DisplayKeyStorageDisabledError) + state.eventSink.invoke(SecureBackupRootEvent.DisplayKeyStorageDisabledError) } }, ) @@ -212,13 +212,13 @@ fun SecureBackupRootView( async = state.enableAction, progressDialog = { }, onSuccess = { }, - onErrorDismiss = { state.eventSink.invoke(SecureBackupRootEvents.DismissDialog) } + onErrorDismiss = { state.eventSink.invoke(SecureBackupRootEvent.DismissDialog) } ) if (state.displayKeyStorageDisabledError) { ErrorDialog( title = null, content = stringResource(id = R.string.screen_chat_backup_key_storage_disabled_error), - onSubmit = { state.eventSink.invoke(SecureBackupRootEvents.DismissDialog) }, + onSubmit = { state.eventSink.invoke(SecureBackupRootEvent.DismissDialog) }, ) } } @@ -236,7 +236,7 @@ private fun LoadingView() { @PreviewsDayNight @Composable internal fun SecureBackupRootViewPreview( - @PreviewParameter(SecureBackupRootStateProvider::class) state: SecureBackupRootState + @PreviewParameter(SecureBackupRootStatePreviewParam::class) state: SecureBackupRootState ) = ElementPreview { SecureBackupRootView( state = state, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupEvents.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupEvent.kt similarity index 63% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupEvents.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupEvent.kt index f61e65ba61..68221a29e4 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupEvents.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupEvent.kt @@ -8,9 +8,9 @@ package io.element.android.features.securebackup.impl.setup -sealed interface SecureBackupSetupEvents { - data object CreateRecoveryKey : SecureBackupSetupEvents - data object RecoveryKeyHasBeenSaved : SecureBackupSetupEvents - data object Done : SecureBackupSetupEvents - data object DismissDialog : SecureBackupSetupEvents +sealed interface SecureBackupSetupEvent { + data object CreateRecoveryKey : SecureBackupSetupEvent + data object RecoveryKeyHasBeenSaved : SecureBackupSetupEvent + data object Done : SecureBackupSetupEvent + data object DismissDialog : SecureBackupSetupEvent } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenter.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenter.kt index 9f27bc9566..538128a0d5 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenter.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenter.kt @@ -53,18 +53,18 @@ class SecureBackupSetupPresenter( } var showSaveConfirmationDialog by remember { mutableStateOf(false) } - fun handleEvent(event: SecureBackupSetupEvents) { + fun handleEvent(event: SecureBackupSetupEvent) { when (event) { - SecureBackupSetupEvents.CreateRecoveryKey -> { + SecureBackupSetupEvent.CreateRecoveryKey -> { coroutineScope.createOrChangeRecoveryKey(stateAndDispatch) } - SecureBackupSetupEvents.RecoveryKeyHasBeenSaved -> + SecureBackupSetupEvent.RecoveryKeyHasBeenSaved -> stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.UserSavedKey) - SecureBackupSetupEvents.DismissDialog -> { + SecureBackupSetupEvent.DismissDialog -> { showSaveConfirmationDialog = false stateAndDispatch.dispatchAction(SecureBackupSetupStateMachine.Event.ClearError) } - SecureBackupSetupEvents.Done -> { + SecureBackupSetupEvent.Done -> { showSaveConfirmationDialog = true } } diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupState.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupState.kt index 752b5e4851..ecec6e3f6a 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupState.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupState.kt @@ -15,7 +15,7 @@ data class SecureBackupSetupState( val recoveryKeyViewState: RecoveryKeyViewState, val showSaveConfirmationDialog: Boolean, val setupState: SetupState, - val eventSink: (SecureBackupSetupEvents) -> Unit + val eventSink: (SecureBackupSetupEvent) -> Unit ) sealed interface SetupState { diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStatePreviewParam.kt similarity index 95% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStateProvider.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStatePreviewParam.kt index a780fa0c36..f88fd56d79 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStateProvider.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupStatePreviewParam.kt @@ -13,7 +13,7 @@ import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyUser import io.element.android.features.securebackup.impl.setup.views.RecoveryKeyViewState import io.element.android.features.securebackup.impl.setup.views.aFormattedRecoveryKey -open class SecureBackupSetupStateProvider : PreviewParameterProvider { +open class SecureBackupSetupStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aSecureBackupSetupState(setupState = SetupState.Init), diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt index 497c64f072..c22bd44c99 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupView.kt @@ -56,7 +56,7 @@ fun SecureBackupSetupView( title = stringResource(id = CommonStrings.common_something_went_wrong), content = stringResource(id = CommonStrings.common_something_went_wrong_message), onSubmit = { - state.eventSink.invoke(SecureBackupSetupEvents.DismissDialog) + state.eventSink.invoke(SecureBackupSetupEvent.DismissDialog) }, ) } @@ -68,7 +68,7 @@ fun SecureBackupSetupView( submitText = stringResource(id = CommonStrings.action_continue), onSubmitClick = onSuccess, onDismiss = { - state.eventSink.invoke(SecureBackupSetupEvents.DismissDialog) + state.eventSink.invoke(SecureBackupSetupEvent.DismissDialog) } ) } @@ -123,12 +123,12 @@ private fun Content( text = formattedRecoveryKey, toastMessage = toastMessage, ) - state.eventSink.invoke(SecureBackupSetupEvents.RecoveryKeyHasBeenSaved) + state.eventSink.invoke(SecureBackupSetupEvent.RecoveryKeyHasBeenSaved) } } else { if (!state.recoveryKeyViewState.inProgress) { { - state.eventSink.invoke(SecureBackupSetupEvents.CreateRecoveryKey) + state.eventSink.invoke(SecureBackupSetupEvent.CreateRecoveryKey) } } else { null @@ -174,7 +174,7 @@ private fun ColumnScope.Buttons( chooserTitle = chooserTitle, text = state.setupState.recoveryKey()!!, ) - state.eventSink.invoke(SecureBackupSetupEvents.RecoveryKeyHasBeenSaved) + state.eventSink.invoke(SecureBackupSetupEvent.RecoveryKeyHasBeenSaved) }, ) Button( @@ -184,7 +184,7 @@ private fun ColumnScope.Buttons( if (state.setupState is SetupState.CreatedAndSaved) { onFinish() } else { - state.eventSink.invoke(SecureBackupSetupEvents.Done) + state.eventSink.invoke(SecureBackupSetupEvent.Done) } }, ) @@ -195,7 +195,7 @@ private fun ColumnScope.Buttons( @PreviewsDayNight @Composable internal fun SecureBackupSetupViewPreview( - @PreviewParameter(SecureBackupSetupStateProvider::class) state: SecureBackupSetupState + @PreviewParameter(SecureBackupSetupStatePreviewParam::class) state: SecureBackupSetupState ) = ElementPreview { SecureBackupSetupView( state = state, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupViewChangePreview.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupViewChangePreview.kt index 39fa7debe9..0fd7901f55 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupViewChangePreview.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupViewChangePreview.kt @@ -17,7 +17,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight @PreviewsDayNight @Composable internal fun SecureBackupSetupViewChangePreview( - @PreviewParameter(SecureBackupSetupStateProvider::class) state: SecureBackupSetupState + @PreviewParameter(SecureBackupSetupStatePreviewParam::class) state: SecureBackupSetupState ) = ElementPreview { SecureBackupSetupView( state = state.copy( diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt index 8055709f20..61b36d3adf 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyView.kt @@ -269,7 +269,7 @@ private fun RecoveryKeyFooter(state: RecoveryKeyViewState) { @PreviewsDayNight @Composable internal fun RecoveryKeyViewPreview( - @PreviewParameter(RecoveryKeyViewStateProvider::class) state: RecoveryKeyViewState + @PreviewParameter(RecoveryKeyViewStatePreviewParam::class) state: RecoveryKeyViewState ) = ElementPreview { RecoveryKeyView( state = state, diff --git a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyViewStateProvider.kt b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyViewStatePreviewParam.kt similarity index 95% rename from features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyViewStateProvider.kt rename to features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyViewStatePreviewParam.kt index c1a841aa7b..69a5757e1f 100644 --- a/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyViewStateProvider.kt +++ b/features/securebackup/impl/src/main/kotlin/io/element/android/features/securebackup/impl/setup/views/RecoveryKeyViewStatePreviewParam.kt @@ -10,7 +10,7 @@ package io.element.android.features.securebackup.impl.setup.views import androidx.compose.ui.tooling.preview.PreviewParameterProvider -open class RecoveryKeyViewStateProvider : PreviewParameterProvider { +open class RecoveryKeyViewStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf(RecoveryKeyUserStory.Setup, RecoveryKeyUserStory.Change, RecoveryKeyUserStory.Enter) .flatMap { diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenterTest.kt index 899726d806..69f54fc253 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenterTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/disable/SecureBackupDisablePresenterTest.kt @@ -47,7 +47,7 @@ class SecureBackupDisablePresenterTest { }.test { val initialState = awaitItem() assertThat(initialState.disableAction).isEqualTo(AsyncAction.Uninitialized) - initialState.eventSink(SecureBackupDisableEvents.DisableBackup) + initialState.eventSink(SecureBackupDisableEvent.DisableBackup) val loadingState = awaitItem() assertThat(loadingState.disableAction).isInstanceOf(AsyncAction.Loading::class.java) val finalState = awaitItem() @@ -68,12 +68,12 @@ class SecureBackupDisablePresenterTest { }.test { val initialState = awaitItem() assertThat(initialState.disableAction).isEqualTo(AsyncAction.Uninitialized) - initialState.eventSink(SecureBackupDisableEvents.DisableBackup) + initialState.eventSink(SecureBackupDisableEvent.DisableBackup) val loadingState = awaitItem() assertThat(loadingState.disableAction).isInstanceOf(AsyncAction.Loading::class.java) val errorState = awaitItem() assertThat(errorState.disableAction).isInstanceOf(AsyncAction.Failure::class.java) - errorState.eventSink(SecureBackupDisableEvents.DismissDialogs) + errorState.eventSink(SecureBackupDisableEvent.DismissDialogs) val finalState = awaitItem() assertThat(finalState.disableAction).isEqualTo(AsyncAction.Uninitialized) } diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenterTest.kt index a6ffb57dc4..a1c7d697d6 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenterTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyPresenterTest.kt @@ -56,7 +56,7 @@ class SecureBackupEnterRecoveryKeyPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink(SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange("1234")) + initialState.eventSink(SecureBackupEnterRecoveryKeyEvent.OnRecoveryKeyChange("1234")) val withRecoveryKeyState = awaitItem() assertThat(withRecoveryKeyState.isSubmitEnabled).isTrue() assertThat(withRecoveryKeyState.recoveryKeyViewState).isEqualTo( @@ -68,19 +68,19 @@ class SecureBackupEnterRecoveryKeyPresenterTest { ) ) encryptionService.givenRecoverFailure(AN_EXCEPTION) - withRecoveryKeyState.eventSink(SecureBackupEnterRecoveryKeyEvents.Submit) + withRecoveryKeyState.eventSink(SecureBackupEnterRecoveryKeyEvent.Submit) val loadingState = awaitItem() assertThat(loadingState.submitAction).isEqualTo(AsyncAction.Loading) assertThat(loadingState.isSubmitEnabled).isFalse() val errorState = awaitItem() assertThat(errorState.submitAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION)) assertThat(errorState.isSubmitEnabled).isFalse() - errorState.eventSink(SecureBackupEnterRecoveryKeyEvents.ClearDialog) + errorState.eventSink(SecureBackupEnterRecoveryKeyEvent.ClearDialog) val clearedState = awaitItem() assertThat(clearedState.submitAction).isEqualTo(AsyncAction.Uninitialized) assertThat(clearedState.isSubmitEnabled).isTrue() encryptionService.givenRecoverFailure(null) - clearedState.eventSink(SecureBackupEnterRecoveryKeyEvents.Submit) + clearedState.eventSink(SecureBackupEnterRecoveryKeyEvent.Submit) val loadingState2 = awaitItem() assertThat(loadingState2.submitAction).isEqualTo(AsyncAction.Loading) assertThat(loadingState2.isSubmitEnabled).isFalse() diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyViewTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyViewTest.kt index fcf050f925..5bc880a685 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyViewTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/enter/SecureBackupEnterRecoveryKeyViewTest.kt @@ -58,32 +58,32 @@ class SecureBackupEnterRecoveryKeyViewTest : RobolectricTest() { @Test @Config(qualifiers = "h1024dp") fun `tapping on Continue when key is valid - calls expected action`() = runAndroidComposeUiTest { - val recorder = EventsRecorder() + val recorder = EventsRecorder() setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder), ) clickOn(CommonStrings.action_continue) - recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.Submit) + recorder.assertSingle(SecureBackupEnterRecoveryKeyEvent.Submit) } @Test fun `entering a char emits the expected event`() = runAndroidComposeUiTest { - val recorder = EventsRecorder() + val recorder = EventsRecorder() val keyValue = aFormattedRecoveryKey() setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder), ) onNodeWithText(keyValue).performTextInput("X") recorder.assertSingle( - SecureBackupEnterRecoveryKeyEvents.OnRecoveryKeyChange("X$keyValue") + SecureBackupEnterRecoveryKeyEvent.OnRecoveryKeyChange("X$keyValue") ) } @Test @Config(qualifiers = "h1024dp") fun `toggling the visibility of the textfield changes it`() = runAndroidComposeUiTest { - val recorder = EventsRecorder() + val recorder = EventsRecorder() val keyValue = aFormattedRecoveryKey() setSecureBackupEnterRecoveryKeyView(aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder)) @@ -94,18 +94,18 @@ class SecureBackupEnterRecoveryKeyViewTest : RobolectricTest() { waitForIdle() - recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.ChangeRecoveryKeyFieldContentsVisibility(false)) + recorder.assertSingle(SecureBackupEnterRecoveryKeyEvent.ChangeRecoveryKeyFieldContentsVisibility(false)) } @Test fun `validating from keyboard emits the expected event`() = runAndroidComposeUiTest { - val recorder = EventsRecorder() + val recorder = EventsRecorder() val keyValue = aFormattedRecoveryKey() setSecureBackupEnterRecoveryKeyView( aSecureBackupEnterRecoveryKeyState(isSubmitEnabled = true, eventSink = recorder), ) onNodeWithText(keyValue).performImeAction() - recorder.assertSingle(SecureBackupEnterRecoveryKeyEvents.Submit) + recorder.assertSingle(SecureBackupEnterRecoveryKeyEvent.Submit) } @Test diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenterTest.kt index 63adfc789e..bbee28803e 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenterTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/root/SecureBackupRootPresenterTest.kt @@ -66,7 +66,7 @@ class SecureBackupRootPresenterTest { val errorState = awaitItem() assertThat(errorState.doesBackupExistOnServer).isEqualTo(AsyncData.Failure(AN_EXCEPTION)) encryptionService.givenDoesBackupExistOnServerResult(Result.success(false)) - errorState.eventSink.invoke(SecureBackupRootEvents.RetryKeyBackupState) + errorState.eventSink.invoke(SecureBackupRootEvent.RetryKeyBackupState) val loadingState2 = awaitItem() assertThat(loadingState2.doesBackupExistOnServer).isInstanceOf(AsyncData.Loading::class.java) val finalState = awaitItem() @@ -82,9 +82,9 @@ class SecureBackupRootPresenterTest { }.test { skipItems(2) val initialState = awaitItem() - initialState.eventSink(SecureBackupRootEvents.DisplayKeyStorageDisabledError) + initialState.eventSink(SecureBackupRootEvent.DisplayKeyStorageDisabledError) assertThat(awaitItem().displayKeyStorageDisabledError).isTrue() - initialState.eventSink(SecureBackupRootEvents.DismissDialog) + initialState.eventSink(SecureBackupRootEvent.DismissDialog) assertThat(awaitItem().displayKeyStorageDisabledError).isFalse() } } @@ -97,7 +97,7 @@ class SecureBackupRootPresenterTest { }.test { skipItems(2) val initialState = awaitItem() - initialState.eventSink(SecureBackupRootEvents.EnableKeyStorage) + initialState.eventSink(SecureBackupRootEvent.EnableKeyStorage) assertThat(awaitItem().enableAction.isLoading()).isTrue() assertThat(awaitItem().enableAction.isSuccess()).isTrue() } diff --git a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenterTest.kt b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenterTest.kt index 4aede3c534..6ab0b257a2 100644 --- a/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenterTest.kt +++ b/features/securebackup/impl/src/test/kotlin/io/element/android/features/securebackup/impl/setup/SecureBackupSetupPresenterTest.kt @@ -58,7 +58,7 @@ class SecureBackupSetupPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink.invoke(SecureBackupSetupEvents.CreateRecoveryKey) + initialState.eventSink.invoke(SecureBackupSetupEvent.CreateRecoveryKey) val creatingState = awaitItem() assertThat(creatingState.setupState).isEqualTo(SetupState.Creating) assertThat(creatingState.recoveryKeyViewState).isEqualTo( @@ -80,13 +80,13 @@ class SecureBackupSetupPresenterTest { inProgress = false, ) ) - createdState.eventSink.invoke(SecureBackupSetupEvents.RecoveryKeyHasBeenSaved) + createdState.eventSink.invoke(SecureBackupSetupEvent.RecoveryKeyHasBeenSaved) val createdAndSaveState = awaitItem() assertThat(createdAndSaveState.setupState).isInstanceOf(SetupState.CreatedAndSaved::class.java) - createdAndSaveState.eventSink.invoke(SecureBackupSetupEvents.Done) + createdAndSaveState.eventSink.invoke(SecureBackupSetupEvent.Done) val doneState = awaitItem() assertThat(doneState.showSaveConfirmationDialog).isTrue() - doneState.eventSink.invoke(SecureBackupSetupEvents.DismissDialog) + doneState.eventSink.invoke(SecureBackupSetupEvent.DismissDialog) val doneStateCancelled = awaitItem() assertThat(doneStateCancelled.showSaveConfirmationDialog).isFalse() } @@ -130,12 +130,12 @@ class SecureBackupSetupPresenterTest { assertThat(initialState.isChangeRecoveryKeyUserStory).isFalse() assertThat(initialState.setupState).isEqualTo(SetupState.Init) - initialState.eventSink(SecureBackupSetupEvents.CreateRecoveryKey) + initialState.eventSink(SecureBackupSetupEvent.CreateRecoveryKey) val creatingState = awaitItem() assertThat(creatingState.setupState).isEqualTo(SetupState.Creating) val failedState = awaitItem() assertThat(failedState.setupState).isInstanceOf(SetupState.Error::class.java) - failedState.eventSink(SecureBackupSetupEvents.DismissDialog) + failedState.eventSink(SecureBackupSetupEvent.DismissDialog) val finalState = awaitItem() assertThat(finalState.setupState).isEqualTo(SetupState.Init) @@ -153,7 +153,7 @@ class SecureBackupSetupPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink.invoke(SecureBackupSetupEvents.CreateRecoveryKey) + initialState.eventSink.invoke(SecureBackupSetupEvent.CreateRecoveryKey) val creatingState = awaitItem() assertThat(creatingState.setupState).isEqualTo(SetupState.Creating) assertThat(creatingState.recoveryKeyViewState).isEqualTo( @@ -174,13 +174,13 @@ class SecureBackupSetupPresenterTest { inProgress = false, ) ) - createdState.eventSink.invoke(SecureBackupSetupEvents.RecoveryKeyHasBeenSaved) + createdState.eventSink.invoke(SecureBackupSetupEvent.RecoveryKeyHasBeenSaved) val createdAndSaveState = awaitItem() assertThat(createdAndSaveState.setupState).isInstanceOf(SetupState.CreatedAndSaved::class.java) - createdAndSaveState.eventSink.invoke(SecureBackupSetupEvents.Done) + createdAndSaveState.eventSink.invoke(SecureBackupSetupEvent.Done) val doneState = awaitItem() assertThat(doneState.showSaveConfirmationDialog).isTrue() - doneState.eventSink.invoke(SecureBackupSetupEvents.DismissDialog) + doneState.eventSink.invoke(SecureBackupSetupEvent.DismissDialog) val doneStateCancelled = awaitItem() assertThat(doneStateCancelled.showSaveConfirmationDialog).isFalse() } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressEvents.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressEvent.kt similarity index 70% rename from features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressEvents.kt rename to features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressEvent.kt index e42236d31c..e9cac38806 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressEvents.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressEvent.kt @@ -8,8 +8,8 @@ package io.element.android.features.securityandprivacy.impl.editroomaddress -sealed interface EditRoomAddressEvents { - data object Save : EditRoomAddressEvents - data object DismissError : EditRoomAddressEvents - data class RoomAddressChanged(val roomAddress: String) : EditRoomAddressEvents +sealed interface EditRoomAddressEvent { + data object Save : EditRoomAddressEvent + data object DismissError : EditRoomAddressEvent + data class RoomAddressChanged(val roomAddress: String) : EditRoomAddressEvent } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressPresenter.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressPresenter.kt index 8dcea0d880..df4c4a88e1 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressPresenter.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressPresenter.kt @@ -63,17 +63,17 @@ class EditRoomAddressPresenter( ) } - fun handleEvent(event: EditRoomAddressEvents) { + fun handleEvent(event: EditRoomAddressEvent) { when (event) { - EditRoomAddressEvents.Save -> coroutineScope.save( + EditRoomAddressEvent.Save -> coroutineScope.save( saveAction = saveAction, serverName = homeserverName, newRoomAddress = newRoomAddress ) - is EditRoomAddressEvents.RoomAddressChanged -> { + is EditRoomAddressEvent.RoomAddressChanged -> { newRoomAddress = event.roomAddress } - EditRoomAddressEvents.DismissError -> { + EditRoomAddressEvent.DismissError -> { saveAction.value = AsyncAction.Uninitialized } } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressState.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressState.kt index 89315ab8ce..a9a4b9c669 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressState.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressState.kt @@ -16,7 +16,7 @@ data class EditRoomAddressState( val roomAddress: String, val roomAddressValidity: RoomAddressValidity, val saveAction: AsyncAction, - val eventSink: (EditRoomAddressEvents) -> Unit + val eventSink: (EditRoomAddressEvent) -> Unit ) { val canBeSaved = roomAddressValidity == RoomAddressValidity.Valid } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressStatePreviewParam.kt similarity index 91% rename from features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressStateProvider.kt rename to features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressStatePreviewParam.kt index 7b82175bb7..577bfc9957 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressStatePreviewParam.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.ui.room.address.RoomAddressValidity -open class EditRoomAddressStateProvider : PreviewParameterProvider { +open class EditRoomAddressStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( anEditRoomAddressState(), @@ -28,7 +28,7 @@ fun anEditRoomAddressState( roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Unknown, homeserverName: String = ":myserver.org", saveAction: AsyncAction = AsyncAction.Uninitialized, - eventSink: (EditRoomAddressEvents) -> Unit = {} + eventSink: (EditRoomAddressEvent) -> Unit = {} ) = EditRoomAddressState( roomAddress = roomAddress, roomAddressValidity = roomAddressValidity, diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressView.kt index da18b963fa..9eb15b7af9 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressView.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressView.kt @@ -46,7 +46,7 @@ fun EditRoomAddressView( isSaveActionEnabled = state.canBeSaved, onBackClick = onBackClick, onSaveClick = { - state.eventSink(EditRoomAddressEvents.Save) + state.eventSink(EditRoomAddressEvent.Save) }, ) } @@ -63,7 +63,7 @@ fun EditRoomAddressView( homeserverName = state.homeserverName, addressValidity = state.roomAddressValidity, onAddressChange = { - state.eventSink(EditRoomAddressEvents.RoomAddressChanged(it)) + state.eventSink(EditRoomAddressEvent.RoomAddressChanged(it)) }, label = stringResource(R.string.screen_edit_room_address_title), supportingText = stringResource(R.string.screen_edit_room_address_room_address_section_footer), @@ -81,8 +81,8 @@ fun EditRoomAddressView( }, onSuccess = {}, errorMessage = { stringResource(CommonStrings.error_unknown) }, - onRetry = { state.eventSink(EditRoomAddressEvents.Save) }, - onErrorDismiss = { state.eventSink(EditRoomAddressEvents.DismissError) }, + onRetry = { state.eventSink(EditRoomAddressEvent.Save) }, + onErrorDismiss = { state.eventSink(EditRoomAddressEvent.DismissError) }, ) } } @@ -112,7 +112,7 @@ private fun EditRoomAddressTopBar( @PreviewsDayNight @Composable internal fun EditRoomAddressViewPreview( - @PreviewParameter(EditRoomAddressStateProvider::class) state: EditRoomAddressState + @PreviewParameter(EditRoomAddressStatePreviewParam::class) state: EditRoomAddressState ) = ElementPreview { EditRoomAddressView( state = state, diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStatePreviewParam.kt similarity index 94% rename from features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt rename to features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStatePreviewParam.kt index d2fec941ff..d5d1986b83 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesStatePreviewParam.kt @@ -15,7 +15,7 @@ import io.element.android.libraries.previewutils.room.aSpaceRoom import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet -open class ManageAuthorizedSpacesStateProvider : PreviewParameterProvider { +open class ManageAuthorizedSpacesStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aManageAuthorizedSpacesState(), diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt index 514c38a3fd..0bb6bc49e6 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/manageauthorizedspaces/ManageAuthorizedSpacesView.kt @@ -191,7 +191,7 @@ private fun ManageAuthorizedSpacesTopBar( @PreviewsDayNight @Composable internal fun ManageAuthorizedSpacesViewPreview( - @PreviewParameter(ManageAuthorizedSpacesStateProvider::class) state: ManageAuthorizedSpacesState + @PreviewParameter(ManageAuthorizedSpacesStatePreviewParam::class) state: ManageAuthorizedSpacesState ) = ElementPreview { ManageAuthorizedSpacesView(state = state) } diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStatePreviewParam.kt similarity index 98% rename from features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt rename to features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStatePreviewParam.kt index 998d61b564..102adb4015 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStateProvider.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyStatePreviewParam.kt @@ -16,7 +16,7 @@ import io.element.android.libraries.matrix.api.spaces.SpaceRoom import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableSet -open class SecurityAndPrivacyStateProvider : PreviewParameterProvider { +open class SecurityAndPrivacyStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = commonSecurityAndPrivacyStates(isSpace = false) + commonSecurityAndPrivacyStates(isSpace = true) + diff --git a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt index 43feb99611..e2cc5522a6 100644 --- a/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt +++ b/features/securityandprivacy/impl/src/main/kotlin/io/element/android/features/securityandprivacy/impl/root/SecurityAndPrivacyView.kt @@ -469,12 +469,12 @@ private fun HistoryVisibilityItem( @PreviewWithLargeHeight @Composable -internal fun SecurityAndPrivacyViewLightPreview(@PreviewParameter(SecurityAndPrivacyStateProvider::class) state: SecurityAndPrivacyState) = +internal fun SecurityAndPrivacyViewLightPreview(@PreviewParameter(SecurityAndPrivacyStatePreviewParam::class) state: SecurityAndPrivacyState) = ElementPreviewLight { ContentToPreview(state) } @PreviewWithLargeHeight @Composable -internal fun SecurityAndPrivacyViewDarkPreview(@PreviewParameter(SecurityAndPrivacyStateProvider::class) state: SecurityAndPrivacyState) = +internal fun SecurityAndPrivacyViewDarkPreview(@PreviewParameter(SecurityAndPrivacyStatePreviewParam::class) state: SecurityAndPrivacyState) = ElementPreviewDark { ContentToPreview(state) } @ExcludeFromCoverage diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressPresenterTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressPresenterTest.kt index aac25ec018..9d2b7f9228 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressPresenterTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressPresenterTest.kt @@ -97,7 +97,7 @@ class EditRoomAddressPresenterTest { val presenter = createEditRoomAddressPresenter(roomAliasHelper = roomAliasHelper) presenter.test { with(awaitItem()) { - eventSink(EditRoomAddressEvents.RoomAddressChanged("invalid")) + eventSink(EditRoomAddressEvent.RoomAddressChanged("invalid")) } with(awaitItem()) { assertThat(roomAddress).isEqualTo("invalid") @@ -115,7 +115,7 @@ class EditRoomAddressPresenterTest { val presenter = createEditRoomAddressPresenter() presenter.test { with(awaitItem()) { - eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) + eventSink(EditRoomAddressEvent.RoomAddressChanged("valid")) } with(awaitItem()) { assertThat(roomAddress).isEqualTo("valid") @@ -134,7 +134,7 @@ class EditRoomAddressPresenterTest { val presenter = createEditRoomAddressPresenter(client = client) presenter.test { with(awaitItem()) { - eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) + eventSink(EditRoomAddressEvent.RoomAddressChanged("valid")) } with(awaitItem()) { assertThat(roomAddress).isEqualTo("valid") @@ -163,13 +163,13 @@ class EditRoomAddressPresenterTest { val presenter = createEditRoomAddressPresenter(room = room, navigator = navigator) presenter.test { with(awaitItem()) { - eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) + eventSink(EditRoomAddressEvent.RoomAddressChanged("valid")) } skipItems(1) with(awaitItem()) { assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) assertThat(canBeSaved).isTrue() - eventSink(EditRoomAddressEvents.Save) + eventSink(EditRoomAddressEvent.Save) } with(awaitItem()) { assertThat(saveAction).isEqualTo(AsyncAction.Loading) @@ -212,13 +212,13 @@ class EditRoomAddressPresenterTest { val presenter = createEditRoomAddressPresenter(room = room, navigator = navigator) presenter.test { with(awaitItem()) { - eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) + eventSink(EditRoomAddressEvent.RoomAddressChanged("valid")) } skipItems(1) with(awaitItem()) { assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) assertThat(canBeSaved).isTrue() - eventSink(EditRoomAddressEvents.Save) + eventSink(EditRoomAddressEvent.Save) } with(awaitItem()) { assertThat(saveAction).isEqualTo(AsyncAction.Loading) @@ -263,13 +263,13 @@ class EditRoomAddressPresenterTest { val presenter = createEditRoomAddressPresenter(room = room, navigator = navigator) presenter.test { with(awaitItem()) { - eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) + eventSink(EditRoomAddressEvent.RoomAddressChanged("valid")) } skipItems(1) with(awaitItem()) { assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) assertThat(canBeSaved).isTrue() - eventSink(EditRoomAddressEvents.Save) + eventSink(EditRoomAddressEvent.Save) } with(awaitItem()) { assertThat(saveAction).isEqualTo(AsyncAction.Loading) @@ -309,13 +309,13 @@ class EditRoomAddressPresenterTest { ) presenter.test { with(awaitItem()) { - eventSink(EditRoomAddressEvents.RoomAddressChanged("valid")) + eventSink(EditRoomAddressEvent.RoomAddressChanged("valid")) } skipItems(1) with(awaitItem()) { assertThat(roomAddressValidity).isEqualTo(RoomAddressValidity.Valid) assertThat(canBeSaved).isTrue() - eventSink(EditRoomAddressEvents.Save) + eventSink(EditRoomAddressEvent.Save) } with(awaitItem()) { assertThat(saveAction).isEqualTo(AsyncAction.Loading) @@ -339,12 +339,12 @@ class EditRoomAddressPresenterTest { ) presenter.test { with(awaitItem()) { - eventSink(EditRoomAddressEvents.Save) + eventSink(EditRoomAddressEvent.Save) } assertThat(awaitItem().saveAction).isInstanceOf(AsyncAction.Loading::class.java) with(awaitItem()) { assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java) - eventSink(EditRoomAddressEvents.DismissError) + eventSink(EditRoomAddressEvent.DismissError) } with(awaitItem()) { assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized) diff --git a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressViewTest.kt b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressViewTest.kt index 1d0886f52a..beeab1cd14 100644 --- a/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressViewTest.kt +++ b/features/securityandprivacy/impl/src/test/kotlin/io/element/android/features/securityandprivacy/impl/editroomaddress/EditRoomAddressViewTest.kt @@ -39,7 +39,7 @@ class EditRoomAddressViewTest : RobolectricTest() { @Test fun `click on disabled save doesn't emit event`() = runAndroidComposeUiTest { - val recorder = EventsRecorder(expectEvents = false) + val recorder = EventsRecorder(expectEvents = false) val state = anEditRoomAddressState(eventSink = recorder) setEditRoomAddressView(state) clickOn(CommonStrings.action_save) @@ -48,7 +48,7 @@ class EditRoomAddressViewTest : RobolectricTest() { @Test fun `click on enabled save emits the expected event`() = runAndroidComposeUiTest { - val recorder = EventsRecorder() + val recorder = EventsRecorder() val state = anEditRoomAddressState( roomAddress = "room", roomAddressValidity = RoomAddressValidity.Valid, @@ -56,12 +56,12 @@ class EditRoomAddressViewTest : RobolectricTest() { ) setEditRoomAddressView(state) clickOn(CommonStrings.action_save) - recorder.assertSingle(EditRoomAddressEvents.Save) + recorder.assertSingle(EditRoomAddressEvent.Save) } @Test fun `text changes on text field emits the expected event`() = runAndroidComposeUiTest { - val recorder = EventsRecorder() + val recorder = EventsRecorder() val state = anEditRoomAddressState( roomAddress = "", eventSink = recorder @@ -69,12 +69,12 @@ class EditRoomAddressViewTest : RobolectricTest() { setEditRoomAddressView(state) onNodeWithTag(TestTags.roomAddressField.value).performTextInput("alias") - recorder.assertSingle(EditRoomAddressEvents.RoomAddressChanged("alias")) + recorder.assertSingle(EditRoomAddressEvent.RoomAddressChanged("alias")) } @Test fun `click on dismiss error emits the expected event`() = runAndroidComposeUiTest { - val recorder = EventsRecorder() + val recorder = EventsRecorder() val state = anEditRoomAddressState( roomAddress = "", saveAction = AsyncAction.Failure(IllegalStateException()), @@ -82,12 +82,12 @@ class EditRoomAddressViewTest : RobolectricTest() { ) setEditRoomAddressView(state) clickOn(CommonStrings.action_cancel) - recorder.assertSingle(EditRoomAddressEvents.DismissError) + recorder.assertSingle(EditRoomAddressEvent.DismissError) } @Test fun `click on retry error emits the expected event`() = runAndroidComposeUiTest { - val recorder = EventsRecorder() + val recorder = EventsRecorder() val state = anEditRoomAddressState( roomAddress = "", saveAction = AsyncAction.Failure(IllegalStateException()), @@ -95,7 +95,7 @@ class EditRoomAddressViewTest : RobolectricTest() { ) setEditRoomAddressView(state) clickOn(CommonStrings.action_retry) - recorder.assertSingle(EditRoomAddressEvents.Save) + recorder.assertSingle(EditRoomAddressEvent.Save) } } diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/DefaultShareIntentHandler.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/DefaultShareIntentHandler.kt index cfa06c3a97..2ae31fb00c 100644 --- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/DefaultShareIntentHandler.kt +++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/DefaultShareIntentHandler.kt @@ -42,7 +42,8 @@ class DefaultShareIntentHandler( val uris = getIncomingUris(intent, type) return when { uris.isEmpty() && type == MimeTypes.PlainText -> handlePlainText(intent) - type.isMimeTypeImage() || + uris.isNotEmpty() || + type.isMimeTypeImage() || type.isMimeTypeVideo() || type.isMimeTypeAudio() || type.isMimeTypeApplication() || diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareEvents.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareEvent.kt similarity index 80% rename from features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareEvents.kt rename to features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareEvent.kt index d0e246fe44..5137c50f2d 100644 --- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareEvents.kt +++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareEvent.kt @@ -8,6 +8,6 @@ package io.element.android.features.share.impl -sealed interface ShareEvents { - data object ClearError : ShareEvents +sealed interface ShareEvent { + data object ClearError : ShareEvent } diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt index 93a9ae3bf4..3bd9b98227 100644 --- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt +++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt @@ -55,9 +55,9 @@ class SharePresenter( @Composable override fun present(): ShareState { - fun handleEvent(event: ShareEvents) { + fun handleEvent(event: ShareEvent) { when (event) { - ShareEvents.ClearError -> shareActionState.value = AsyncAction.Uninitialized + ShareEvent.ClearError -> shareActionState.value = AsyncAction.Uninitialized } } diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareState.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareState.kt index fd98d58ba3..7538309941 100644 --- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareState.kt +++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareState.kt @@ -13,5 +13,5 @@ import io.element.android.libraries.matrix.api.core.RoomId data class ShareState( val shareAction: AsyncAction>, - val eventSink: (ShareEvents) -> Unit + val eventSink: (ShareEvent) -> Unit ) diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareStateProvider.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareStatePreviewParam.kt similarity index 90% rename from features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareStateProvider.kt rename to features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareStatePreviewParam.kt index 88f9d42f40..18258f1b95 100644 --- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareStateProvider.kt +++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareStatePreviewParam.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 ShareStateProvider : PreviewParameterProvider { +open class ShareStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aShareState(), @@ -32,7 +32,7 @@ open class ShareStateProvider : PreviewParameterProvider { fun aShareState( shareAction: AsyncAction> = AsyncAction.Uninitialized, - eventSink: (ShareEvents) -> Unit = {} + eventSink: (ShareEvent) -> Unit = {} ) = ShareState( shareAction = shareAction, eventSink = eventSink diff --git a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareView.kt b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareView.kt index ec5897bcad..2516c31a66 100644 --- a/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareView.kt +++ b/features/share/impl/src/main/kotlin/io/element/android/features/share/impl/ShareView.kt @@ -26,14 +26,14 @@ fun ShareView( onShareSuccess(it) }, onErrorDismiss = { - state.eventSink(ShareEvents.ClearError) + state.eventSink(ShareEvent.ClearError) }, ) } @PreviewsDayNight @Composable -internal fun ShareViewPreview(@PreviewParameter(ShareStateProvider::class) state: ShareState) = ElementPreview { +internal fun ShareViewPreview(@PreviewParameter(ShareStatePreviewParam::class) state: ShareState) = ElementPreview { ShareView( state = state, onShareSuccess = {} diff --git a/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/DefaultShareIntentHandlerTest.kt b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/DefaultShareIntentHandlerTest.kt new file mode 100644 index 0000000000..d81bbe053d --- /dev/null +++ b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/DefaultShareIntentHandlerTest.kt @@ -0,0 +1,303 @@ +/* + * 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.share.impl + +import android.content.ComponentName +import android.content.ContentProvider +import android.content.ContentValues +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.content.IntentFilter +import android.database.Cursor +import android.net.Uri +import androidx.core.net.toUri +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import io.element.android.features.share.api.ShareIntentData +import io.element.android.features.share.api.UriToShare +import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.tests.testutils.robolectric.RobolectricTest +import org.junit.Test +import org.robolectric.Robolectric +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows.shadowOf + +class DefaultShareIntentHandlerTest : RobolectricTest() { + @Test + fun `an image is shared as an uri`() { + val uri = "content://sender/image.jpg".toUri() + val result = createDefaultShareIntentHandler().handleIncomingShareIntent( + aSendIntent(type = "image/jpeg", uri = uri) + ) + assertThat(result).isEqualTo( + ShareIntentData.Uris( + text = null, + uris = listOf(UriToShare(uri = uri, mimeType = "image/jpeg")), + ) + ) + } + + @Test + fun `an uri with an unlisted mime type is shared`() { + val uri = "content://sender/logs.zip".toUri() + val result = createDefaultShareIntentHandler().handleIncomingShareIntent( + aSendIntent(type = "message/rfc822", uri = uri) + ) + assertThat(result).isEqualTo( + ShareIntentData.Uris( + text = null, + uris = listOf(UriToShare(uri = uri, mimeType = "message/rfc822")), + ) + ) + } + + @Test + fun `an uri with an unlisted mime type keeps the text as a caption`() { + val uri = "content://sender/logs.zip".toUri() + val result = createDefaultShareIntentHandler().handleIncomingShareIntent( + aSendIntent(type = "message/rfc822", uri = uri).apply { + putExtra(Intent.EXTRA_TEXT, "a caption") + } + ) + assertThat(result).isEqualTo( + ShareIntentData.Uris( + text = "a caption", + uris = listOf(UriToShare(uri = uri, mimeType = "message/rfc822")), + ) + ) + } + + @Test + fun `several uris with an unlisted mime type are shared`() { + val firstUri = "content://sender/first.zip".toUri() + val secondUri = "content://sender/second.zip".toUri() + val intent = Intent(Intent.ACTION_SEND_MULTIPLE).apply { + type = "message/rfc822" + putParcelableArrayListExtra(Intent.EXTRA_STREAM, arrayListOf(firstUri, secondUri)) + } + val result = createDefaultShareIntentHandler().handleIncomingShareIntent(intent) + assertThat(result).isEqualTo( + ShareIntentData.Uris( + text = null, + uris = listOf( + UriToShare(uri = firstUri, mimeType = "message/rfc822"), + UriToShare(uri = secondUri, mimeType = "message/rfc822"), + ), + ) + ) + } + + @Test + fun `a plain text is shared as a text`() { + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, "a text") + } + val result = createDefaultShareIntentHandler().handleIncomingShareIntent(intent) + assertThat(result).isEqualTo(ShareIntentData.PlainText("a text")) + } + + @Test + fun `an intent with neither uri nor text is not handled`() { + val intent = Intent(Intent.ACTION_SEND).apply { + type = "message/rfc822" + } + val result = createDefaultShareIntentHandler().handleIncomingShareIntent(intent) + assertThat(result).isNull() + } + + @Test + fun `an intent without mime type is not handled`() { + val result = createDefaultShareIntentHandler().handleIncomingShareIntent(Intent(Intent.ACTION_SEND)) + assertThat(result).isNull() + } + + @Test + fun `an intent with a listed mime type and no uri is handled`() { + listOf( + MimeTypes.Jpeg, + MimeTypes.Mp4, + MimeTypes.Mp3, + MimeTypes.Pdf, + "file/binary", + "text/html", + MimeTypes.Any, + ).forEach { type -> + val result = createDefaultShareIntentHandler().handleIncomingShareIntent( + Intent(Intent.ACTION_SEND).apply { this.type = type } + ) + assertWithMessage("$type was not properly handled").that(result).isEqualTo( + ShareIntentData.Uris( + text = null, + uris = emptyList(), + ) + ) + } + } + + @Test + fun `a send multiple intent without uri is handled`() { + val intent = Intent(Intent.ACTION_SEND_MULTIPLE).apply { + type = MimeTypes.Jpeg + } + val result = createDefaultShareIntentHandler().handleIncomingShareIntent(intent) + assertThat(result).isEqualTo( + ShareIntentData.Uris( + text = null, + uris = emptyList(), + ) + ) + } + + @Test + fun `an uri is ignored when the intent is neither a send nor a send multiple`() { + val intent = Intent(Intent.ACTION_VIEW).apply { + type = MimeTypes.Jpeg + putExtra(Intent.EXTRA_STREAM, "content://sender/image.jpg".toUri()) + } + val result = createDefaultShareIntentHandler().handleIncomingShareIntent(intent) + assertThat(result).isEqualTo( + ShareIntentData.Uris( + text = null, + uris = emptyList(), + ) + ) + } + + @Test + fun `an empty text is not kept as a caption`() { + val uri = "content://sender/logs.zip".toUri() + val result = createDefaultShareIntentHandler().handleIncomingShareIntent( + aSendIntent(type = "message/rfc822", uri = uri).apply { + putExtra(Intent.EXTRA_TEXT, "") + } + ) + assertThat(result).isEqualTo( + ShareIntentData.Uris( + text = null, + uris = listOf(UriToShare(uri = uri, mimeType = "message/rfc822")), + ) + ) + } + + @Test + fun `a plain text intent without text is not handled`() { + val intent = Intent(Intent.ACTION_SEND).apply { + type = MimeTypes.PlainText + } + val result = createDefaultShareIntentHandler().handleIncomingShareIntent(intent) + assertThat(result).isNull() + } + + @Test + fun `a plain text intent with an empty text is not handled`() { + val intent = Intent(Intent.ACTION_SEND).apply { + type = MimeTypes.PlainText + putExtra(Intent.EXTRA_TEXT, "") + } + val result = createDefaultShareIntentHandler().handleIncomingShareIntent(intent) + assertThat(result).isNull() + } + + @Test + fun `the mime type of the uri overrides the one of the intent`() { + Robolectric.setupContentProvider(ZipContentProvider::class.java, "sender") + val uri = "content://sender/logs.zip".toUri() + val result = createDefaultShareIntentHandler().handleIncomingShareIntent( + aSendIntent(type = MimeTypes.Any, uri = uri) + ) + assertThat(result).isEqualTo( + ShareIntentData.Uris( + text = null, + uris = listOf(UriToShare(uri = uri, mimeType = "application/zip")), + ) + ) + } + + @Test + fun `the intent is made explicit for the application which will read the uri`() { + val intent = aSendIntent(type = "message/rfc822", uri = "content://sender/logs.zip".toUri()) + givenAnApplicationHandling(intent) + createDefaultShareIntentHandler().handleIncomingShareIntent(intent) + assertThat(intent.action).isNull() + assertThat(intent.component).isEqualTo(ComponentName(A_PACKAGE_NAME, AN_ACTIVITY_NAME)) + } + + @Test + fun `the intent stays implicit when the uri permission cannot be granted`() { + val uri = "content://sender/logs.zip".toUri() + val intent = aSendIntent(type = "message/rfc822", uri = uri) + givenAnApplicationHandling(intent) + val result = DefaultShareIntentHandler( + context = FailingGrantUriPermissionContext(RuntimeEnvironment.getApplication()), + ).handleIncomingShareIntent(intent) + assertThat(intent.action).isEqualTo(Intent.ACTION_SEND) + assertThat(intent.component).isNull() + assertThat(result).isEqualTo( + ShareIntentData.Uris( + text = null, + uris = listOf(UriToShare(uri = uri, mimeType = "message/rfc822")), + ) + ) + } + + private fun aSendIntent(type: String, uri: Uri) = Intent(Intent.ACTION_SEND).apply { + this.type = type + putExtra(Intent.EXTRA_STREAM, uri) + } + + private fun givenAnApplicationHandling(intent: Intent) { + val componentName = ComponentName(A_PACKAGE_NAME, AN_ACTIVITY_NAME) + shadowOf(RuntimeEnvironment.getApplication().packageManager).apply { + addActivityIfNotPresent(componentName) + addIntentFilterForActivity( + componentName, + IntentFilter(intent.action).apply { + addCategory(Intent.CATEGORY_DEFAULT) + addDataType(intent.type) + } + ) + } + } + + private fun createDefaultShareIntentHandler() = DefaultShareIntentHandler( + context = RuntimeEnvironment.getApplication(), + ) + + private class FailingGrantUriPermissionContext(context: Context) : ContextWrapper(context) { + override fun grantUriPermission(toPackage: String, uri: Uri, modeFlags: Int) { + error("Unable to grant Uri permission") + } + } + + private class ZipContentProvider : ContentProvider() { + override fun onCreate() = true + + override fun getType(uri: Uri) = "application/zip" + + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String?, + ): Cursor? = null + + override fun insert(uri: Uri, values: ContentValues?): Uri? = null + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?) = 0 + + override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?) = 0 + } + + private companion object { + const val A_PACKAGE_NAME = "io.element.android.x" + const val AN_ACTIVITY_NAME = "io.element.android.x.MainActivity" + } +} diff --git a/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt index ab3a260c01..44ccea92d6 100644 --- a/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt +++ b/features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt @@ -65,7 +65,7 @@ class SharePresenterTest : RobolectricTest() { assertThat(awaitItem().shareAction.isLoading()).isTrue() val failure = awaitItem() assertThat(failure.shareAction.isFailure()).isTrue() - failure.eventSink.invoke(ShareEvents.ClearError) + failure.eventSink.invoke(ShareEvent.ClearError) assertThat(awaitItem().shareAction.isUninitialized()).isTrue() } } diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutEvents.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutEvent.kt similarity index 78% rename from features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutEvents.kt rename to features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutEvent.kt index 3b78af9918..1e774488e5 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutEvents.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutEvent.kt @@ -8,6 +8,6 @@ package io.element.android.features.signedout.impl -sealed interface SignedOutEvents { - data object SignInAgain : SignedOutEvents +sealed interface SignedOutEvent { + data object SignInAgain : SignedOutEvent } diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt index 1aa13d9b8d..1f1a9be1d7 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutPresenter.kt @@ -43,9 +43,9 @@ class SignedOutPresenter( }.collectAsState(initial = null) val coroutineScope = rememberCoroutineScope() - fun handleEvent(event: SignedOutEvents) { + fun handleEvent(event: SignedOutEvent) { when (event) { - SignedOutEvents.SignInAgain -> coroutineScope.launch { + SignedOutEvent.SignInAgain -> coroutineScope.launch { sessionStore.removeSession(sessionId.value) } } diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt index bc1ee18f5a..aeff7c7d2f 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutState.kt @@ -13,5 +13,5 @@ import io.element.android.libraries.sessionstorage.api.SessionData data class SignedOutState( val appName: String, val signedOutSession: SessionData?, - val eventSink: (SignedOutEvents) -> Unit, + val eventSink: (SignedOutEvent) -> Unit, ) diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStatePreviewParam.kt similarity index 94% rename from features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt rename to features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStatePreviewParam.kt index 95a57db93e..ae60b7aacb 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStateProvider.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutStatePreviewParam.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.sessionstorage.api.LoginType import io.element.android.libraries.sessionstorage.api.SessionData -open class SignedOutStateProvider : PreviewParameterProvider { +open class SignedOutStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aSignedOutState(), diff --git a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt index 41a99713e7..73e854c836 100644 --- a/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt +++ b/features/signedout/impl/src/main/kotlin/io/element/android/features/signedout/impl/SignedOutView.kt @@ -39,7 +39,7 @@ fun SignedOutView( state: SignedOutState, modifier: Modifier = Modifier, ) { - BackHandler(onBack = { state.eventSink(SignedOutEvents.SignInAgain) }) + BackHandler(onBack = { state.eventSink(SignedOutEvent.SignInAgain) }) HeaderFooterPage( modifier = modifier .fillMaxSize() @@ -49,7 +49,7 @@ fun SignedOutView( content = { SignedOutContent() }, footer = { SignedOutFooter( - onSignInAgain = { state.eventSink(SignedOutEvents.SignInAgain) }, + onSignInAgain = { state.eventSink(SignedOutEvent.SignInAgain) }, ) } ) @@ -109,7 +109,7 @@ private fun SignedOutFooter( @PreviewsDayNight @Composable internal fun SignedOutViewPreview( - @PreviewParameter(SignedOutStateProvider::class) state: SignedOutState, + @PreviewParameter(SignedOutStatePreviewParam::class) state: SignedOutState, ) = ElementPreview { SignedOutView( state = state, diff --git a/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt index 519016b1e0..b72ebbcdba 100644 --- a/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt +++ b/features/signedout/impl/src/test/kotlin/io/element/android/features/signedout/impl/SignedOutPresenterTest.kt @@ -60,7 +60,7 @@ class SignedOutPresenterTest { assertThat(initialState.signedOutSession).isEqualTo(aSessionData) assertThat(sessionStore.getAllSessions()).isNotEmpty() assertThat(sessionStore.numberOfSessions()).isEqualTo(1) - initialState.eventSink(SignedOutEvents.SignInAgain) + initialState.eventSink(SignedOutEvent.SignInAgain) assertThat(awaitItem().signedOutSession).isNull() assertThat(sessionStore.getAllSessions()).isEmpty() assertThat(sessionStore.numberOfSessions()).isEqualTo(0) diff --git a/features/signedout/test/build.gradle.kts b/features/signedout/test/build.gradle.kts new file mode 100644 index 0000000000..5bbfb33c1a --- /dev/null +++ b/features/signedout/test/build.gradle.kts @@ -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. + */ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.signedout.test" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.features.signedout.api) + implementation(projects.tests.testutils) +} diff --git a/features/signedout/test/src/main/kotlin/io/element/android/features/signedout/test/FakeSignedOutEntryPoint.kt b/features/signedout/test/src/main/kotlin/io/element/android/features/signedout/test/FakeSignedOutEntryPoint.kt new file mode 100644 index 0000000000..863b946871 --- /dev/null +++ b/features/signedout/test/src/main/kotlin/io/element/android/features/signedout/test/FakeSignedOutEntryPoint.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.signedout.test + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.features.signedout.api.SignedOutEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeSignedOutEntryPoint( + private val createNodeResult: (BuildContext, SignedOutEntryPoint.Params) -> Node = { _, _ -> lambdaError() }, +) : SignedOutEntryPoint { + override fun createNode( + parentNode: Node, + buildContext: BuildContext, + params: SignedOutEntryPoint.Params, + ): Node = createNodeResult(buildContext, params) +} diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStatePreviewParam.kt similarity index 97% rename from features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt rename to features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStatePreviewParam.kt index add57bd4c4..9f9652c65c 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceStatePreviewParam.kt @@ -17,7 +17,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -internal class AddRoomToSpaceStateProvider : PreviewParameterProvider { +internal class AddRoomToSpaceStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( // Initial state with suggestions diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt index 2f75072d0a..4db17634d6 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/addroom/AddRoomToSpaceView.kt @@ -273,7 +273,7 @@ private fun RoomListItem( @PreviewsDayNight @Composable internal fun AddRoomToSpaceViewPreview( - @PreviewParameter(AddRoomToSpaceStateProvider::class) state: AddRoomToSpaceState + @PreviewParameter(AddRoomToSpaceStatePreviewParam::class) state: AddRoomToSpaceState ) = ElementPreview { AddRoomToSpaceView( state = state, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceEvent.kt similarity index 59% rename from features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceEvents.kt rename to features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceEvent.kt index 8bc96c428c..debec67151 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceEvents.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceEvent.kt @@ -10,11 +10,11 @@ package io.element.android.features.space.impl.leave import io.element.android.libraries.matrix.api.core.RoomId -sealed interface LeaveSpaceEvents { - data object Retry : LeaveSpaceEvents - data object SelectAllRooms : LeaveSpaceEvents - data object DeselectAllRooms : LeaveSpaceEvents - data class ToggleRoomSelection(val roomId: RoomId) : LeaveSpaceEvents - data object LeaveSpace : LeaveSpaceEvents - data object CloseError : LeaveSpaceEvents +sealed interface LeaveSpaceEvent { + data object Retry : LeaveSpaceEvent + data object SelectAllRooms : LeaveSpaceEvent + data object DeselectAllRooms : LeaveSpaceEvent + data class ToggleRoomSelection(val roomId: RoomId) : LeaveSpaceEvent + data object LeaveSpace : LeaveSpaceEvent + data object CloseError : LeaveSpaceEvent } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt index e0876c255c..91a4552903 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenter.kt @@ -99,33 +99,33 @@ class LeaveSpacePresenter( } } - fun handleEvent(event: LeaveSpaceEvents) { + fun handleEvent(event: LeaveSpaceEvent) { when (event) { - LeaveSpaceEvents.Retry -> { + LeaveSpaceEvent.Retry -> { leaveSpaceRooms = AsyncData.Loading() retryCount += 1 } - LeaveSpaceEvents.DeselectAllRooms -> { + LeaveSpaceEvent.DeselectAllRooms -> { selectedRoomIds = persistentSetOf() } - LeaveSpaceEvents.SelectAllRooms -> { + LeaveSpaceEvent.SelectAllRooms -> { selectedRoomIds = selectableSpaceRooms.dataOrNull() .orEmpty() .filter { it.isLastOwner.not() } .map { it.spaceRoom.roomId } } - is LeaveSpaceEvents.ToggleRoomSelection -> { + is LeaveSpaceEvent.ToggleRoomSelection -> { selectedRoomIds = if (selectedRoomIds.contains(event.roomId)) { selectedRoomIds - event.roomId } else { selectedRoomIds + event.roomId } } - LeaveSpaceEvents.LeaveSpace -> coroutineScope.leaveSpace( + LeaveSpaceEvent.LeaveSpace -> coroutineScope.leaveSpace( leaveSpaceAction = leaveSpaceAction, selectedRoomIds = selectedRoomIds, ) - LeaveSpaceEvents.CloseError -> { + LeaveSpaceEvent.CloseError -> { leaveSpaceAction.value = AsyncAction.Uninitialized } } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt index 9bf029cb15..5f0423721e 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceState.kt @@ -19,7 +19,7 @@ data class LeaveSpaceState( val areCreatorsPrivileged: Boolean, val selectableSpaceRooms: AsyncData>, val leaveSpaceAction: AsyncAction, - val eventSink: (LeaveSpaceEvents) -> Unit, + val eventSink: (LeaveSpaceEvent) -> Unit, ) { private val rooms = selectableSpaceRooms.dataOrNull().orEmpty().toImmutableList() private val lastAdminRooms: ImmutableList diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStatePreviewParam.kt similarity index 98% rename from features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt rename to features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStatePreviewParam.kt index ddf34cb076..3243a4fa53 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceStatePreviewParam.kt @@ -19,7 +19,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -class LeaveSpaceStateProvider : PreviewParameterProvider { +class LeaveSpaceStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aLeaveSpaceState(), diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt index b4722fad0d..e9e13c2c0e 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/leave/LeaveSpaceView.kt @@ -92,7 +92,7 @@ fun LeaveSpaceView( showLeaveButton = state.showLeaveButton, selectedRoomsCount = state.selectedRoomsCount, onLeaveSpace = { - state.eventSink(LeaveSpaceEvents.LeaveSpace) + state.eventSink(LeaveSpaceEvent.LeaveSpace) }, onCancel = onCancel, showRolesAndPermissionsButton = state.needsOwnerChange && !state.areCreatorsPrivileged, @@ -115,7 +115,7 @@ fun LeaveSpaceView( selectableSpaceRoom = selectableSpaceRoom, showCheckBox = state.hasOnlyLastAdminRoom.not(), onClick = { - state.eventSink(LeaveSpaceEvents.ToggleRoomSelection(selectableSpaceRoom.spaceRoom.roomId)) + state.eventSink(LeaveSpaceEvent.ToggleRoomSelection(selectableSpaceRoom.spaceRoom.roomId)) } ) } @@ -125,7 +125,7 @@ fun LeaveSpaceView( AsyncFailure( throwable = state.selectableSpaceRooms.error, onRetry = { - state.eventSink(LeaveSpaceEvents.Retry) + state.eventSink(LeaveSpaceEvent.Retry) }, ) } @@ -143,7 +143,7 @@ fun LeaveSpaceView( async = state.leaveSpaceAction, onSuccess = { /* Nothing to do, the screen will be dismissed automatically */ }, errorMessage = { stringResource(CommonStrings.error_unknown) }, - onErrorDismiss = { state.eventSink(LeaveSpaceEvents.CloseError) }, + onErrorDismiss = { state.eventSink(LeaveSpaceEvent.CloseError) }, ) } @@ -184,11 +184,11 @@ private fun LeaveSpaceHeader( if (state.showQuickAction) { if (state.areAllSelected) { QuickActionButton(CommonStrings.action_deselect_all) { - state.eventSink(LeaveSpaceEvents.DeselectAllRooms) + state.eventSink(LeaveSpaceEvent.DeselectAllRooms) } } else { QuickActionButton(resId = CommonStrings.action_select_all) { - state.eventSink(LeaveSpaceEvents.SelectAllRooms) + state.eventSink(LeaveSpaceEvent.SelectAllRooms) } } } @@ -366,7 +366,7 @@ private fun SpaceItem( @PreviewsDayNight @Composable internal fun LeaveSpaceViewPreview( - @PreviewParameter(LeaveSpaceStateProvider::class) state: LeaveSpaceState, + @PreviewParameter(LeaveSpaceStatePreviewParam::class) state: LeaveSpaceState, ) = ElementPreview { LeaveSpaceView( state = state, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvent.kt similarity index 54% rename from features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt rename to features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvent.kt index 0f17a2f6f7..1761ec0846 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvents.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceEvent.kt @@ -11,21 +11,21 @@ package io.element.android.features.space.impl.root import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.spaces.SpaceRoom -sealed interface SpaceEvents { - data object LoadMore : SpaceEvents - data class Join(val spaceRoom: SpaceRoom) : SpaceEvents - data object ClearFailures : SpaceEvents - data class AcceptInvite(val spaceRoom: SpaceRoom) : SpaceEvents - data class DeclineInvite(val spaceRoom: SpaceRoom) : SpaceEvents +sealed interface SpaceEvent { + data object LoadMore : SpaceEvent + data class Join(val spaceRoom: SpaceRoom) : SpaceEvent + data object ClearFailures : SpaceEvent + data class AcceptInvite(val spaceRoom: SpaceRoom) : SpaceEvent + data class DeclineInvite(val spaceRoom: SpaceRoom) : SpaceEvent - data class ShowTopicViewer(val topic: String) : SpaceEvents - data object HideTopicViewer : SpaceEvents + data class ShowTopicViewer(val topic: String) : SpaceEvent + data object HideTopicViewer : SpaceEvent // Manage mode events - data object EnterManageMode : SpaceEvents - data object ExitManageMode : SpaceEvents - data class ToggleRoomSelection(val roomId: RoomId) : SpaceEvents - data object ConfirmRoomRemoval : SpaceEvents - data object RemoveSelectedRooms : SpaceEvents - data object ClearRemoveAction : SpaceEvents + data object EnterManageMode : SpaceEvent + data object ExitManageMode : SpaceEvent + data class ToggleRoomSelection(val roomId: RoomId) : SpaceEvent + data object ConfirmRoomRemoval : SpaceEvent + data object RemoveSelectedRooms : SpaceEvent + data object ClearRemoveAction : SpaceEvent } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt index de30ddb6c1..d42501d002 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpacePresenter.kt @@ -22,7 +22,7 @@ import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.JoinedRoom.Trigger 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.libraries.architecture.AsyncAction @@ -150,51 +150,51 @@ class SpacePresenter( } } - fun handleEvent(event: SpaceEvents) { + fun handleEvent(event: SpaceEvent) { when (event) { // SpaceRoomList is loaded automatically as backend is really slow. Event is kept for future. - SpaceEvents.LoadMore -> Unit - is SpaceEvents.Join -> { + SpaceEvent.LoadMore -> Unit + is SpaceEvent.Join -> { sessionCoroutineScope.joinRoom(event.spaceRoom, joinActions, setJoinActions) } - SpaceEvents.ClearFailures -> { + SpaceEvent.ClearFailures -> { val failedActions = joinActions .filterValues { it is AsyncAction.Failure } .mapValues { AsyncAction.Uninitialized } setJoinActions(joinActions + failedActions) } - is SpaceEvents.AcceptInvite -> { + is SpaceEvent.AcceptInvite -> { acceptDeclineInviteState.eventSink( - AcceptDeclineInviteEvents.AcceptInvite(event.spaceRoom.toInviteData()) + AcceptDeclineInviteEvent.AcceptInvite(event.spaceRoom.toInviteData()) ) } - is SpaceEvents.DeclineInvite -> { + is SpaceEvent.DeclineInvite -> { acceptDeclineInviteState.eventSink( - AcceptDeclineInviteEvents.DeclineInvite(invite = event.spaceRoom.toInviteData(), shouldConfirm = true, blockUser = false) + AcceptDeclineInviteEvent.DeclineInvite(invite = event.spaceRoom.toInviteData(), shouldConfirm = true, blockUser = false) ) } - SpaceEvents.HideTopicViewer -> topicViewerState = TopicViewerState.Hidden - is SpaceEvents.ShowTopicViewer -> topicViewerState = TopicViewerState.Shown(event.topic) + SpaceEvent.HideTopicViewer -> topicViewerState = TopicViewerState.Hidden + is SpaceEvent.ShowTopicViewer -> topicViewerState = TopicViewerState.Shown(event.topic) // Manage mode events - SpaceEvents.EnterManageMode -> { + SpaceEvent.EnterManageMode -> { isManageMode = true selectedRoomIds = emptySet() } - SpaceEvents.ExitManageMode -> { + SpaceEvent.ExitManageMode -> { localCoroutineScope.launch { exitManageMode(shouldReset = removedRoomIds.isNotEmpty()) } } - is SpaceEvents.ToggleRoomSelection -> { + is SpaceEvent.ToggleRoomSelection -> { selectedRoomIds = if (event.roomId in selectedRoomIds) { selectedRoomIds - event.roomId } else { selectedRoomIds + event.roomId } } - SpaceEvents.RemoveSelectedRooms -> { + SpaceEvent.RemoveSelectedRooms -> { removeRoomsAction = AsyncAction.ConfirmingNoParams } - SpaceEvents.ConfirmRoomRemoval -> { + SpaceEvent.ConfirmRoomRemoval -> { localCoroutineScope.launch { removeRoomsAction = AsyncAction.Loading val spaceId = spaceRoomList.spaceId @@ -219,7 +219,7 @@ class SpacePresenter( } } } - SpaceEvents.ClearRemoveAction -> { + SpaceEvent.ClearRemoveAction -> { removeRoomsAction = AsyncAction.Uninitialized } } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt index a669c294b5..a782b9fa71 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceState.kt @@ -32,7 +32,7 @@ data class SpaceState( val selectedRoomIds: ImmutableSet, val canEditSpaceGraph: Boolean, val removeRoomsAction: AsyncAction, - val eventSink: (SpaceEvents) -> Unit + val eventSink: (SpaceEvent) -> Unit ) { fun isJoining(spaceId: RoomId): Boolean = joinActions[spaceId] == AsyncAction.Loading fun isSelected(spaceId: RoomId): Boolean = selectedRoomIds.contains(spaceId) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStatePreviewParam.kt similarity index 98% rename from features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt rename to features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStatePreviewParam.kt index 6824cc6c10..ee44c8c17e 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceStatePreviewParam.kt @@ -26,7 +26,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableSet -open class SpaceStateProvider : PreviewParameterProvider { +open class SpaceStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aSpaceState(), @@ -80,7 +80,7 @@ fun aSpaceState( selectedRoomIds: Set = emptySet(), canManageRooms: Boolean = true, removeRoomsAction: AsyncAction = AsyncAction.Uninitialized, - eventSink: (SpaceEvents) -> Unit = { }, + eventSink: (SpaceEvent) -> Unit = { }, ) = SpaceState( spaceInfo = spaceInfo, children = children.toImmutableList(), diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt index fe971091f5..069b00492c 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/root/SpaceView.kt @@ -114,7 +114,7 @@ fun SpaceView( var handledBack by remember { mutableStateOf(false) } BackHandler(enabled = !handledBack) { if (state.isManageMode) { - state.eventSink(SpaceEvents.ExitManageMode) + state.eventSink(SpaceEvent.ExitManageMode) } else { handledBack = true onBackClick() @@ -133,8 +133,8 @@ fun SpaceView( ManageModeTopBar( selectedCount = state.selectedCount, isRemoveButtonEnabled = state.isRemoveButtonEnabled, - onCancelClick = { state.eventSink(SpaceEvents.ExitManageMode) }, - onRemoveClick = { state.eventSink(SpaceEvents.RemoveSelectedRooms) }, + onCancelClick = { state.eventSink(SpaceEvent.ExitManageMode) }, + onRemoveClick = { state.eventSink(SpaceEvent.RemoveSelectedRooms) }, ) } AnimatedVisibility( @@ -152,7 +152,7 @@ fun SpaceView( onSettingsClick = onSettingsClick, onShareSpace = onShareSpace, onViewMembersClick = onViewMembersClick, - onManageRoomsClick = { state.eventSink(SpaceEvents.EnterManageMode) }, + onManageRoomsClick = { state.eventSink(SpaceEvent.EnterManageMode) }, onAddRoomClick = onAddRoomClick, onCreateRoomClick = onCreateRoomClick, ) @@ -168,13 +168,13 @@ fun SpaceView( state = state, onRoomClick = { spaceRoom -> if (state.isManageMode) { - state.eventSink(SpaceEvents.ToggleRoomSelection(spaceRoom.roomId)) + state.eventSink(SpaceEvent.ToggleRoomSelection(spaceRoom.roomId)) } else { onRoomClick(spaceRoom) } }, onTopicClick = { topic -> - state.eventSink(SpaceEvents.ShowTopicViewer(topic)) + state.eventSink(SpaceEvent.ShowTopicViewer(topic)) }, onCreateRoomClick = onCreateRoomClick, onAddRoomClick = onAddRoomClick, @@ -187,8 +187,8 @@ fun SpaceView( spaceDisplayName = state.spaceInfo.name ?: state.spaceInfo.id.value, removeRoomsAction = state.removeRoomsAction, selectedCount = state.selectedCount, - onConfirm = { state.eventSink(SpaceEvents.ConfirmRoomRemoval) }, - onDismiss = { state.eventSink(SpaceEvents.ClearRemoveAction) }, + onConfirm = { state.eventSink(SpaceEvent.ConfirmRoomRemoval) }, + onDismiss = { state.eventSink(SpaceEvent.ClearRemoveAction) }, ) acceptDeclineInviteView() } @@ -198,7 +198,7 @@ fun SpaceView( TopicViewerBottomSheet( topicViewerState = state.topicViewerState, onDismiss = { - state.eventSink(SpaceEvents.HideTopicViewer) + state.eventSink(SpaceEvent.HideTopicViewer) } ) } @@ -207,7 +207,7 @@ fun SpaceView( @Composable private fun JoinFailuresEffect( hasAnyFailure: Boolean, - eventSink: (SpaceEvents) -> Unit, + eventSink: (SpaceEvent) -> Unit, ) { val asyncIndicatorState = rememberAsyncIndicatorState() val updatedEventSink by rememberUpdatedState(eventSink) @@ -218,7 +218,7 @@ private fun JoinFailuresEffect( AsyncIndicator.Failure(text = stringResource(CommonStrings.common_something_went_wrong)) } delay(AsyncIndicator.DURATION_SHORT) - updatedEventSink(SpaceEvents.ClearFailures) + updatedEventSink(SpaceEvent.ClearFailures) } else { asyncIndicatorState.clear() } @@ -317,7 +317,7 @@ private fun SpaceViewContent( } } else { spaceRoom.trailingAction(isCurrentlyJoining = isCurrentlyJoining) { - state.eventSink(SpaceEvents.Join(spaceRoom)) + state.eventSink(SpaceEvent.Join(spaceRoom)) } }, bottomAction = if (state.isManageMode) { @@ -325,10 +325,10 @@ private fun SpaceViewContent( } else { spaceRoom.inviteButtons( onAcceptClick = { - state.eventSink(SpaceEvents.AcceptInvite(spaceRoom)) + state.eventSink(SpaceEvent.AcceptInvite(spaceRoom)) }, onDeclineClick = { - state.eventSink(SpaceEvents.DeclineInvite(spaceRoom)) + state.eventSink(SpaceEvent.DeclineInvite(spaceRoom)) } ) } @@ -384,7 +384,7 @@ private fun EmptySpaceView( @Composable private fun LoadingMoreIndicator( - eventSink: (SpaceEvents) -> Unit, + eventSink: (SpaceEvent) -> Unit, modifier: Modifier = Modifier ) { Box( @@ -397,7 +397,7 @@ private fun LoadingMoreIndicator( ) val latestEventSink by rememberUpdatedState(eventSink) LaunchedEffect(Unit) { - latestEventSink(SpaceEvents.LoadMore) + latestEventSink(SpaceEvent.LoadMore) } } } @@ -680,7 +680,7 @@ private fun RemoveRoomsActionView( @PreviewsDayNight @Composable internal fun SpaceViewPreview( - @PreviewParameter(SpaceStateProvider::class) state: SpaceState + @PreviewParameter(SpaceStatePreviewParam::class) state: SpaceState ) = ElementPreview { SpaceView( state = state, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsEvents.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsEvent.kt similarity index 88% rename from features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsEvents.kt rename to features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsEvent.kt index 3c2b1e6609..d85191ed73 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsEvents.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsEvent.kt @@ -8,4 +8,4 @@ package io.element.android.features.space.impl.settings -sealed interface SpaceSettingsEvents +sealed interface SpaceSettingsEvent diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsState.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsState.kt index 5c7e9b5c6e..01f65e4fdf 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsState.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsState.kt @@ -20,5 +20,5 @@ data class SpaceSettingsState( val canEditDetails: Boolean, val showRolesAndPermissions: Boolean, val showSecurityAndPrivacy: Boolean, - val eventSink: (SpaceSettingsEvents) -> Unit + val eventSink: (SpaceSettingsEvent) -> Unit ) diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsStateProvider.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsStatePreviewParam.kt similarity index 91% rename from features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsStateProvider.kt rename to features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsStatePreviewParam.kt index 36f6a2a1d0..d61b1b656c 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsStateProvider.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsStatePreviewParam.kt @@ -13,7 +13,7 @@ import io.element.android.libraries.designsystem.preview.SPACE_NAME import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId -open class SpaceSettingsStateProvider : PreviewParameterProvider { +open class SpaceSettingsStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aSpaceSettingsState(), @@ -32,7 +32,7 @@ fun aSpaceSettingsState( showRolesAndPermissions: Boolean = false, showSecurityAndPrivacy: Boolean = false, canEditDetails: Boolean = false, - eventSink: (SpaceSettingsEvents) -> Unit = {}, + eventSink: (SpaceSettingsEvent) -> Unit = {}, ) = SpaceSettingsState( roomId = roomId, name = name, diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt index 6237e443b0..b5cfc7702a 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/settings/SpaceSettingsView.kt @@ -223,7 +223,7 @@ private fun LeaveSpaceItem( @PreviewsDayNight @Composable internal fun SpaceSettingsViewPreview( - @PreviewParameter(SpaceSettingsStateProvider::class) state: SpaceSettingsState + @PreviewParameter(SpaceSettingsStatePreviewParam::class) state: SpaceSettingsState ) = ElementPreview { SpaceSettingsView( state = state, diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt index 021f52defd..4f7bb78c80 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/leave/LeaveSpacePresenterTest.kt @@ -61,7 +61,7 @@ class LeaveSpacePresenterTest { val stateError = awaitItem() assertThat(stateError.selectableSpaceRooms.isFailure()).isTrue() // Retry - stateError.eventSink(LeaveSpaceEvents.Retry) + stateError.eventSink(LeaveSpaceEvent.Retry) skipItems(1) val stateLoadingAgain = awaitItem() assertThat(stateLoadingAgain.selectableSpaceRooms.isLoading()).isTrue() @@ -121,7 +121,7 @@ class LeaveSpacePresenterTest { assertThat(finalState.selectableSpaceRooms.dataOrNull()!!.map { it.spaceRoom.roomId }).containsExactly(A_ROOM_ID, A_ROOM_ID_3) assertThat(finalState.selectedRoomsCount).isEqualTo(2) // Leaving the space will not include the DM - finalState.eventSink(LeaveSpaceEvents.LeaveSpace) + finalState.eventSink(LeaveSpaceEvent.LeaveSpace) val stateLeaving = awaitItem() assertThat(stateLeaving.leaveSpaceAction).isEqualTo(AsyncAction.Loading) val stateLeft = awaitItem() @@ -165,32 +165,32 @@ class LeaveSpacePresenterTest { assertThat(room2.isSelected).isFalse() assertThat(room2.isLastOwner).isTrue() // Deselect all - state.eventSink(LeaveSpaceEvents.DeselectAllRooms) + state.eventSink(LeaveSpaceEvent.DeselectAllRooms) skipItems(1) val stateAllDeselected = awaitItem() val dataAllDeselected = stateAllDeselected.selectableSpaceRooms.dataOrNull()!! assertThat(dataAllDeselected.any { it.isSelected }).isFalse() // Select all - stateAllDeselected.eventSink(LeaveSpaceEvents.SelectAllRooms) + stateAllDeselected.eventSink(LeaveSpaceEvent.SelectAllRooms) skipItems(1) val stateAllSelected = awaitItem() val dataAllSelected = stateAllSelected.selectableSpaceRooms.dataOrNull()!! // The last admin room should not be selected assertThat(dataAllSelected.count { it.isSelected }).isEqualTo(1) // Toggle selection of the first room - stateAllSelected.eventSink(LeaveSpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + stateAllSelected.eventSink(LeaveSpaceEvent.ToggleRoomSelection(A_ROOM_ID)) skipItems(1) val stateOneDeselected = awaitItem() val dataOneDeselected = stateOneDeselected.selectableSpaceRooms.dataOrNull()!! assertThat(dataOneDeselected[0].isSelected).isFalse() // Toggle selection of the first room - stateOneDeselected.eventSink(LeaveSpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + stateOneDeselected.eventSink(LeaveSpaceEvent.ToggleRoomSelection(A_ROOM_ID)) skipItems(1) val stateOneSelected = awaitItem() val dataOneSelected = stateOneSelected.selectableSpaceRooms.dataOrNull()!! assertThat(dataOneSelected[0].isSelected).isTrue() // Leave space - stateOneSelected.eventSink(LeaveSpaceEvents.LeaveSpace) + stateOneSelected.eventSink(LeaveSpaceEvent.LeaveSpace) val stateLeaving = awaitItem() assertThat(stateLeaving.leaveSpaceAction).isEqualTo(AsyncAction.Loading) val stateLeft = awaitItem() @@ -215,13 +215,13 @@ class LeaveSpacePresenterTest { presenter.test { skipItems(3) val state = awaitItem() - state.eventSink(LeaveSpaceEvents.LeaveSpace) + state.eventSink(LeaveSpaceEvent.LeaveSpace) val stateLeaving = awaitItem() assertThat(stateLeaving.leaveSpaceAction).isEqualTo(AsyncAction.Loading) val stateError = awaitItem() assertThat(stateError.leaveSpaceAction.isFailure()).isTrue() // Close error - stateError.eventSink(LeaveSpaceEvents.CloseError) + stateError.eventSink(LeaveSpaceEvent.CloseError) val stateErrorClosed = awaitItem() assertThat(stateErrorClosed.leaveSpaceAction).isEqualTo(AsyncAction.Uninitialized) } diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt index 2de52a600b..627ea4483d 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpacePresenterTest.kt @@ -15,7 +15,7 @@ import com.google.testing.junit.testparameterinjector.KotlinTestParameters.named import com.google.testing.junit.testparameterinjector.TestParameter import com.google.testing.junit.testparameterinjector.TestParameterInjector 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 @@ -106,7 +106,7 @@ class SpacePresenterTest { presenter.test { val state = awaitItem() // LoadMore event should not cause any state change - state.eventSink(SpaceEvents.LoadMore) + state.eventSink(SpaceEvent.LoadMore) expectNoEvents() } } @@ -181,7 +181,7 @@ class SpacePresenterTest { skipItems(1) val state = awaitItem() assertThat(state.joinActions[A_ROOM_ID_2]).isNull() - state.eventSink(SpaceEvents.Join(aNotJoinedRoom)) + state.eventSink(SpaceEvent.Join(aNotJoinedRoom)) val joiningState = awaitItem() assertThat(joiningState.joinActions[A_ROOM_ID_2]).isEqualTo(AsyncAction.Loading) // Let the joinRoom call complete @@ -234,14 +234,14 @@ class SpacePresenterTest { skipItems(1) val state = awaitItem() assertThat(state.joinActions[A_ROOM_ID_2]).isNull() - state.eventSink(SpaceEvents.Join(aNotJoinedRoom)) + state.eventSink(SpaceEvent.Join(aNotJoinedRoom)) val joiningState = awaitItem() assertThat(joiningState.joinActions[A_ROOM_ID_2]).isEqualTo(AsyncAction.Loading) val errorState = awaitItem() // Joined room is removed from the join actions assertThat(errorState.joinActions[A_ROOM_ID_2]!!.isFailure()).isTrue() // Clear error - errorState.eventSink(SpaceEvents.ClearFailures) + errorState.eventSink(SpaceEvent.ClearFailures) val clearedState = awaitItem() assertThat(clearedState.joinActions[A_ROOM_ID_2]).isEqualTo(AsyncAction.Uninitialized) } @@ -258,9 +258,9 @@ class SpacePresenterTest { val state = awaitItem() assertThat(state.topicViewerState).isEqualTo(TopicViewerState.Hidden) advanceUntilIdle() - state.eventSink(SpaceEvents.ShowTopicViewer("topic")) + state.eventSink(SpaceEvent.ShowTopicViewer("topic")) assertThat(awaitItem().topicViewerState).isEqualTo(TopicViewerState.Shown("topic")) - state.eventSink(SpaceEvents.HideTopicViewer) + state.eventSink(SpaceEvent.HideTopicViewer) assertThat(awaitItem().topicViewerState).isEqualTo(TopicViewerState.Hidden) } } @@ -272,7 +272,7 @@ class SpacePresenterTest { "decline" to false, ), ) = runTest { - val eventRecorder = EventsRecorder() + val eventRecorder = EventsRecorder() val anInvitedRoom = aSpaceRoom( roomId = A_ROOM_ID_2, state = CurrentUserMembership.INVITED, @@ -300,16 +300,16 @@ class SpacePresenterTest { val state = awaitItem() assertThat(state.joinActions[A_ROOM_ID_2]).isNull() if (acceptInvite) { - state.eventSink(SpaceEvents.AcceptInvite(anInvitedRoom)) + state.eventSink(SpaceEvent.AcceptInvite(anInvitedRoom)) eventRecorder.assertSingle( - AcceptDeclineInviteEvents.AcceptInvite( + AcceptDeclineInviteEvent.AcceptInvite( invite = anInvitedRoom.toInviteData(), ) ) } else { - state.eventSink(SpaceEvents.DeclineInvite(anInvitedRoom)) + state.eventSink(SpaceEvent.DeclineInvite(anInvitedRoom)) eventRecorder.assertSingle( - AcceptDeclineInviteEvents.DeclineInvite( + AcceptDeclineInviteEvent.DeclineInvite( invite = anInvitedRoom.toInviteData(), shouldConfirm = true, blockUser = false, @@ -325,7 +325,7 @@ class SpacePresenterTest { presenter.test { val state = awaitItem() assertThat(state.isManageMode).isFalse() - state.eventSink(SpaceEvents.EnterManageMode) + state.eventSink(SpaceEvent.EnterManageMode) val manageModeState = awaitItem() assertThat(manageModeState.isManageMode).isTrue() assertThat(manageModeState.selectedRoomIds).isEmpty() @@ -342,9 +342,9 @@ class SpacePresenterTest { val presenter = createSpacePresenter(spaceRoomList = fakeSpaceRoomList) presenter.test { val initialState = awaitItem() - initialState.eventSink(SpaceEvents.EnterManageMode) - initialState.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) - initialState.eventSink(SpaceEvents.ExitManageMode) + initialState.eventSink(SpaceEvent.EnterManageMode) + initialState.eventSink(SpaceEvent.ToggleRoomSelection(A_ROOM_ID)) + initialState.eventSink(SpaceEvent.ExitManageMode) advanceUntilIdle() val finalState = expectMostRecentItem() assertThat(finalState.isManageMode).isFalse() @@ -359,13 +359,13 @@ class SpacePresenterTest { val presenter = createSpacePresenter() presenter.test { val initialState = awaitItem() - initialState.eventSink(SpaceEvents.EnterManageMode) + initialState.eventSink(SpaceEvent.EnterManageMode) // Select a room - initialState.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + initialState.eventSink(SpaceEvent.ToggleRoomSelection(A_ROOM_ID)) var latestState = expectMostRecentItem() assertThat(latestState.selectedRoomIds).containsExactly(A_ROOM_ID) // Deselect the room - latestState.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + latestState.eventSink(SpaceEvent.ToggleRoomSelection(A_ROOM_ID)) latestState = expectMostRecentItem() assertThat(latestState.selectedRoomIds).isEmpty() } @@ -397,10 +397,10 @@ class SpacePresenterTest { advanceUntilIdle() val stateWithChildren = awaitItem() assertThat(stateWithChildren.children).hasSize(1) - stateWithChildren.eventSink(SpaceEvents.EnterManageMode) - stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) - stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms) - stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval) + stateWithChildren.eventSink(SpaceEvent.EnterManageMode) + stateWithChildren.eventSink(SpaceEvent.ToggleRoomSelection(A_ROOM_ID)) + stateWithChildren.eventSink(SpaceEvent.RemoveSelectedRooms) + stateWithChildren.eventSink(SpaceEvent.ConfirmRoomRemoval) advanceUntilIdle() val successState = expectMostRecentItem() assertThat(successState.removeRoomsAction).isEqualTo(AsyncAction.Success(Unit)) @@ -442,11 +442,11 @@ class SpacePresenterTest { advanceUntilIdle() val stateWithChildren = awaitItem() assertThat(stateWithChildren.children).hasSize(2) - stateWithChildren.eventSink(SpaceEvents.EnterManageMode) - stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) - stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID_2)) - stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms) - stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval) + stateWithChildren.eventSink(SpaceEvent.EnterManageMode) + stateWithChildren.eventSink(SpaceEvent.ToggleRoomSelection(A_ROOM_ID)) + stateWithChildren.eventSink(SpaceEvent.ToggleRoomSelection(A_ROOM_ID_2)) + stateWithChildren.eventSink(SpaceEvent.RemoveSelectedRooms) + stateWithChildren.eventSink(SpaceEvent.ConfirmRoomRemoval) advanceUntilIdle() val failureState = expectMostRecentItem() assertThat(failureState.removeRoomsAction.isFailure()).isTrue() @@ -492,16 +492,16 @@ class SpacePresenterTest { awaitItem() // Initial empty state advanceUntilIdle() val stateWithChildren = awaitItem() - stateWithChildren.eventSink(SpaceEvents.EnterManageMode) - stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) - stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID_2)) - stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms) - stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval) + stateWithChildren.eventSink(SpaceEvent.EnterManageMode) + stateWithChildren.eventSink(SpaceEvent.ToggleRoomSelection(A_ROOM_ID)) + stateWithChildren.eventSink(SpaceEvent.ToggleRoomSelection(A_ROOM_ID_2)) + stateWithChildren.eventSink(SpaceEvent.RemoveSelectedRooms) + stateWithChildren.eventSink(SpaceEvent.ConfirmRoomRemoval) advanceUntilIdle() val failureState = expectMostRecentItem() assertThat(failureState.removeRoomsAction.isFailure()).isTrue() // Exit manage mode after partial failure - reset should be called - failureState.eventSink(SpaceEvents.ExitManageMode) + failureState.eventSink(SpaceEvent.ExitManageMode) advanceUntilIdle() expectMostRecentItem() assert(resetResult).isCalledOnce() @@ -530,7 +530,7 @@ class SpacePresenterTest { // Both room and space visible initially assertThat(stateWithChildren.children).hasSize(2) assertThat(stateWithChildren.isManageMode).isFalse() - stateWithChildren.eventSink(SpaceEvents.EnterManageMode) + stateWithChildren.eventSink(SpaceEvent.EnterManageMode) val manageModeState = expectMostRecentItem() // Only rooms visible in manage mode assertThat(manageModeState.children).hasSize(1) @@ -648,12 +648,12 @@ class SpacePresenterTest { awaitItem() // Initial empty state advanceUntilIdle() val stateWithChildren = awaitItem() - stateWithChildren.eventSink(SpaceEvents.EnterManageMode) + stateWithChildren.eventSink(SpaceEvent.EnterManageMode) // Select both rooms for removal - stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) - stateWithChildren.eventSink(SpaceEvents.ToggleRoomSelection(A_ROOM_ID_2)) - stateWithChildren.eventSink(SpaceEvents.RemoveSelectedRooms) - stateWithChildren.eventSink(SpaceEvents.ConfirmRoomRemoval) + stateWithChildren.eventSink(SpaceEvent.ToggleRoomSelection(A_ROOM_ID)) + stateWithChildren.eventSink(SpaceEvent.ToggleRoomSelection(A_ROOM_ID_2)) + stateWithChildren.eventSink(SpaceEvent.RemoveSelectedRooms) + stateWithChildren.eventSink(SpaceEvent.ConfirmRoomRemoval) advanceUntilIdle() val failureState = expectMostRecentItem() assertThat(failureState.removeRoomsAction.isFailure()).isTrue() diff --git a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt index 042d6a00ff..218e635e0c 100644 --- a/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt +++ b/features/space/impl/src/test/kotlin/io/element/android/features/space/impl/root/SpaceViewTest.kt @@ -42,7 +42,7 @@ import org.robolectric.annotation.Config class SpaceViewTest : RobolectricTest() { @Test fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { setSpaceView( aSpaceState( @@ -58,7 +58,7 @@ class SpaceViewTest : RobolectricTest() { @Test fun `clicking on a room name invokes the expected callback`() = runAndroidComposeUiTest { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, displayName = A_ROOM_NAME) - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(aSpaceRoom) { setSpaceView( aSpaceState( @@ -75,7 +75,7 @@ class SpaceViewTest : RobolectricTest() { @Test fun `clicking on Join room emits the expected Event`() = runAndroidComposeUiTest { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = null) - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setSpaceView( aSpaceState( children = listOf(aSpaceRoom), @@ -84,14 +84,14 @@ class SpaceViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_join) - eventsRecorder.assertSingle(SpaceEvents.Join(aSpaceRoom)) + eventsRecorder.assertSingle(SpaceEvent.Join(aSpaceRoom)) } @Config(qualifiers = "h1024dp") @Test fun `clicking on accept invite emits the expected Event`() = runAndroidComposeUiTest { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED) - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setSpaceView( aSpaceState( hasMoreToLoad = false, @@ -100,14 +100,14 @@ class SpaceViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_accept) - eventsRecorder.assertSingle(SpaceEvents.AcceptInvite(aSpaceRoom)) + eventsRecorder.assertSingle(SpaceEvent.AcceptInvite(aSpaceRoom)) } @Config(qualifiers = "h1024dp") @Test fun `clicking on decline invite emits the expected Event`() = runAndroidComposeUiTest { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, state = CurrentUserMembership.INVITED) - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setSpaceView( aSpaceState( hasMoreToLoad = false, @@ -116,13 +116,13 @@ class SpaceViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_decline) - eventsRecorder.assertSingle(SpaceEvents.DeclineInvite(aSpaceRoom)) + eventsRecorder.assertSingle(SpaceEvent.DeclineInvite(aSpaceRoom)) } @Config(qualifiers = "h1024dp") @Test fun `clicking on topic emits the expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setSpaceView( aSpaceState( spaceInfo = aRoomInfo(topic = A_ROOM_TOPIC), @@ -131,12 +131,12 @@ class SpaceViewTest : RobolectricTest() { ) ) onNodeWithText(A_ROOM_TOPIC).performClick() - eventsRecorder.assertSingle(SpaceEvents.ShowTopicViewer(A_ROOM_TOPIC)) + eventsRecorder.assertSingle(SpaceEvent.ShowTopicViewer(A_ROOM_TOPIC)) } @Test fun `clicking back in manage mode emits ExitManageMode event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setSpaceView( aSpaceState( hasMoreToLoad = false, @@ -145,13 +145,13 @@ class SpaceViewTest : RobolectricTest() { ) ) pressBackKey() - eventsRecorder.assertSingle(SpaceEvents.ExitManageMode) + eventsRecorder.assertSingle(SpaceEvent.ExitManageMode) } @Test fun `clicking on room in manage mode emits ToggleRoomSelection event`() = runAndroidComposeUiTest { val aSpaceRoom = aSpaceRoom(roomId = A_ROOM_ID, displayName = A_ROOM_NAME) - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setSpaceView( aSpaceState( children = listOf(aSpaceRoom), @@ -161,12 +161,12 @@ class SpaceViewTest : RobolectricTest() { ) ) onNodeWithText(A_ROOM_NAME).performClick() - eventsRecorder.assertSingle(SpaceEvents.ToggleRoomSelection(A_ROOM_ID)) + eventsRecorder.assertSingle(SpaceEvent.ToggleRoomSelection(A_ROOM_ID)) } @Test fun `clicking remove button emits RemoveSelectedRooms event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setSpaceView( aSpaceState( children = listOf(aSpaceRoom(roomId = A_ROOM_ID)), @@ -177,13 +177,13 @@ class SpaceViewTest : RobolectricTest() { ) ) clickOn(CommonStrings.action_remove) - eventsRecorder.assertSingle(SpaceEvents.RemoveSelectedRooms) + eventsRecorder.assertSingle(SpaceEvent.RemoveSelectedRooms) } @Config(qualifiers = "h1024dp") @Test fun `clicking confirm in removal dialog emits ConfirmRoomRemoval event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setSpaceView( aSpaceState( children = listOf(aSpaceRoom(roomId = A_ROOM_ID)), @@ -196,7 +196,7 @@ class SpaceViewTest : RobolectricTest() { ) // Click on the Remove button in the confirmation dialog clickOn(CommonStrings.action_remove, inDialog = true) - eventsRecorder.assertSingle(SpaceEvents.ConfirmRoomRemoval) + eventsRecorder.assertSingle(SpaceEvent.ConfirmRoomRemoval) } @Test diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/UserListView.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/UserListView.kt index c8587d9784..e067e13995 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/UserListView.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/components/UserListView.kt @@ -24,9 +24,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import io.element.android.features.startchat.impl.userlist.UserListEvents +import io.element.android.features.startchat.impl.userlist.UserListEvent import io.element.android.features.startchat.impl.userlist.UserListState -import io.element.android.features.startchat.impl.userlist.UserListStateProvider +import io.element.android.features.startchat.impl.userlist.UserListStatePreviewParam import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -63,13 +63,13 @@ fun UserListView( showLoader = state.showSearchLoader, isMultiSelectionEnable = state.isMultiSelectionEnabled, showBackButton = showBackButton, - onActiveChange = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) }, + onActiveChange = { state.eventSink(UserListEvent.OnSearchActiveChanged(it)) }, onUserSelect = { - state.eventSink(UserListEvents.AddToSelection(it)) + state.eventSink(UserListEvent.AddToSelection(it)) onSelectUser(it) }, onUserDeselect = { - state.eventSink(UserListEvents.RemoveFromSelection(it)) + state.eventSink(UserListEvent.RemoveFromSelection(it)) onDeselectUser(it) }, ) @@ -80,7 +80,7 @@ fun UserListView( selectedUsers = state.selectedUsers, autoScroll = true, onUserRemove = { - state.eventSink(UserListEvents.RemoveFromSelection(it)) + state.eventSink(UserListEvent.RemoveFromSelection(it)) onDeselectUser(it) }, ) @@ -104,10 +104,10 @@ fun UserListView( checked = isSelected, onCheckedChange = { if (isSelected) { - state.eventSink(UserListEvents.RemoveFromSelection(recentDirectRoom.matrixUser)) + state.eventSink(UserListEvent.RemoveFromSelection(recentDirectRoom.matrixUser)) onDeselectUser(recentDirectRoom.matrixUser) } else { - state.eventSink(UserListEvents.AddToSelection(recentDirectRoom.matrixUser)) + state.eventSink(UserListEvent.AddToSelection(recentDirectRoom.matrixUser)) onSelectUser(recentDirectRoom.matrixUser) } }, @@ -129,7 +129,7 @@ fun UserListView( @PreviewsDayNight @Composable -internal fun UserListViewPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) = ElementPreview { +internal fun UserListViewPreview(@PreviewParameter(UserListStatePreviewParam::class) state: UserListState) = ElementPreview { UserListView( state = state, onSelectUser = {}, diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressStateProvider.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressStatePreviewParam.kt similarity index 93% rename from features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressStateProvider.kt rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressStatePreviewParam.kt index 5528a84196..3404a737a3 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressStateProvider.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressStatePreviewParam.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias -open class JoinRoomByAddressStateProvider : PreviewParameterProvider { +open class JoinRoomByAddressStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aJoinRoomByAddressState(), diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressView.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressView.kt index ffe6a47f0d..4d2babe70b 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressView.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/joinbyaddress/JoinRoomByAddressView.kt @@ -137,7 +137,7 @@ private fun RoomAddressField( @PreviewsDayNight @Composable internal fun JoinRoomByAddressViewPreview( - @PreviewParameter(JoinRoomByAddressStateProvider::class) state: JoinRoomByAddressState + @PreviewParameter(JoinRoomByAddressStatePreviewParam::class) state: JoinRoomByAddressState ) = ElementPreview { JoinRoomByAddressView(state = state) } diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatEvents.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatEvent.kt similarity index 82% rename from features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatEvents.kt rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatEvent.kt index 66e135b45c..68f50c41ee 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatEvents.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatEvent.kt @@ -10,7 +10,7 @@ package io.element.android.features.startchat.impl.root import io.element.android.libraries.matrix.api.user.MatrixUser -sealed interface StartChatEvents { - data class StartDM(val matrixUser: MatrixUser) : StartChatEvents - data object CancelStartDM : StartChatEvents +sealed interface StartChatEvent { + data class StartDM(val matrixUser: MatrixUser) : StartChatEvent + data object CancelStartDM : StartChatEvent } diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt index 079ece4180..8b1f92222e 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenter.kt @@ -49,16 +49,16 @@ class StartChatPresenter( val localCoroutineScope = rememberCoroutineScope() val startDmActionState: MutableState> = remember { mutableStateOf(AsyncAction.Uninitialized) } - fun handleEvent(event: StartChatEvents) { + fun handleEvent(event: StartChatEvent) { when (event) { - is StartChatEvents.StartDM -> localCoroutineScope.launch { + is StartChatEvent.StartDM -> localCoroutineScope.launch { startDMAction.execute( matrixUser = event.matrixUser, createIfDmDoesNotExist = startDmActionState.value is AsyncAction.Confirming, actionState = startDmActionState, ) } - StartChatEvents.CancelStartDM -> startDmActionState.value = AsyncAction.Uninitialized + StartChatEvent.CancelStartDM -> startDmActionState.value = AsyncAction.Uninitialized } } diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt index e6746e1302..167e4f444a 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatState.kt @@ -16,5 +16,5 @@ data class StartChatState( val applicationName: String, val userListState: UserListState, val startDmAction: AsyncAction, - val eventSink: (StartChatEvents) -> Unit, + val eventSink: (StartChatEvent) -> Unit, ) diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStatePreviewParam.kt similarity index 96% rename from features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStatePreviewParam.kt index 41a3551d1d..bd7c24da41 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStateProvider.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatStatePreviewParam.kt @@ -21,7 +21,7 @@ import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.usersearch.api.UserSearchResult import kotlinx.collections.immutable.persistentListOf -open class StartChatStateProvider : PreviewParameterProvider { +open class StartChatStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aCreateRoomRootState(), @@ -72,7 +72,7 @@ fun aCreateRoomRootState( applicationName: String = "Element X Preview", userListState: UserListState = aUserListState(), startDmAction: AsyncAction = AsyncAction.Uninitialized, - eventSink: (StartChatEvents) -> Unit = {}, + eventSink: (StartChatEvent) -> Unit = {}, ) = StartChatState( applicationName = applicationName, userListState = userListState, diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt index 104a8cecf7..3ef807b777 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/root/StartChatView.kt @@ -94,7 +94,7 @@ fun StartChatView( onSelectUser = { coroutineScope.launch { view.hideKeyboardAndAwaitAnimation() - state.eventSink(StartChatEvents.StartDM(it)) + state.eventSink(StartChatEvent.StartDM(it)) } }, onDeselectUser = { }, @@ -124,21 +124,21 @@ fun StartChatView( errorMessage = { stringResource(R.string.screen_start_chat_error_starting_chat) }, onRetry = { state.userListState.selectedUsers.firstOrNull() - ?.let { state.eventSink(StartChatEvents.StartDM(it)) } + ?.let { state.eventSink(StartChatEvent.StartDM(it)) } // Cancel start DM if there is no more selected user (should not happen) - ?: state.eventSink(StartChatEvents.CancelStartDM) + ?: state.eventSink(StartChatEvent.CancelStartDM) }, - onErrorDismiss = { state.eventSink(StartChatEvents.CancelStartDM) }, + onErrorDismiss = { state.eventSink(StartChatEvent.CancelStartDM) }, confirmationDialog = { data -> if (data is ConfirmingStartDmWithMatrixUser) { CreateDmConfirmationBottomSheet( matrixUser = data.matrixUser, isUserIdentityUnknown = data.isUserIdentityUnknown, onSendInvite = { - state.eventSink(StartChatEvents.StartDM(data.matrixUser)) + state.eventSink(StartChatEvent.StartDM(data.matrixUser)) }, onDismiss = { - state.eventSink(StartChatEvents.CancelStartDM) + state.eventSink(StartChatEvent.CancelStartDM) }, ) } @@ -255,7 +255,7 @@ private fun CreateRoomActionButton( @PreviewsDayNight @Composable -internal fun StartChatViewPreview(@PreviewParameter(StartChatStateProvider::class) state: StartChatState) = +internal fun StartChatViewPreview(@PreviewParameter(StartChatStatePreviewParam::class) state: StartChatState) = ElementPreview { StartChatView( state = state, diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt index 286f0abe68..adbab19180 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenter.kt @@ -85,11 +85,11 @@ class DefaultUserListPresenter( }.launchIn(this) } - fun handleEvent(event: UserListEvents) { + fun handleEvent(event: UserListEvent) { when (event) { - is UserListEvents.OnSearchActiveChanged -> isSearchActive = event.active - is UserListEvents.AddToSelection -> userListDataStore.selectUser(event.matrixUser) - is UserListEvents.RemoveFromSelection -> userListDataStore.removeUserFromSelection(event.matrixUser) + is UserListEvent.OnSearchActiveChanged -> isSearchActive = event.active + is UserListEvent.AddToSelection -> userListDataStore.selectUser(event.matrixUser) + is UserListEvent.RemoveFromSelection -> userListDataStore.removeUserFromSelection(event.matrixUser) } } diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListEvents.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListEvent.kt similarity index 88% rename from features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListEvents.kt rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListEvent.kt index f7601f6d36..098e4411d2 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListEvents.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListEvent.kt @@ -10,8 +10,8 @@ package io.element.android.features.startchat.impl.userlist import io.element.android.libraries.matrix.api.user.MatrixUser -sealed interface UserListEvents { - data class AddToSelection(val matrixUser: MatrixUser) : UserListEvents - data class RemoveFromSelection(val matrixUser: MatrixUser) : UserListEvents - data class OnSearchActiveChanged(val active: Boolean) : UserListEvents +sealed interface UserListEvent { + data class AddToSelection(val matrixUser: MatrixUser) : UserListEvent + data class RemoveFromSelection(val matrixUser: MatrixUser) : UserListEvent + data class OnSearchActiveChanged(val active: Boolean) : UserListEvent } diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListState.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListState.kt index 6a507f8a75..6ce5bafddf 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListState.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListState.kt @@ -23,7 +23,7 @@ data class UserListState( val isSearchActive: Boolean, val selectionMode: SelectionMode, val recentDirectRooms: ImmutableList, - val eventSink: (UserListEvents) -> Unit, + val eventSink: (UserListEvent) -> Unit, ) { val isMultiSelectionEnabled = selectionMode == SelectionMode.Multiple } diff --git a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListStateProvider.kt b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListStatePreviewParam.kt similarity index 96% rename from features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListStateProvider.kt rename to features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListStatePreviewParam.kt index 91d047d238..a84927be62 100644 --- a/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListStateProvider.kt +++ b/features/startchat/impl/src/main/kotlin/io/element/android/features/startchat/impl/userlist/UserListStatePreviewParam.kt @@ -19,7 +19,7 @@ import io.element.android.libraries.usersearch.api.UserSearchResult import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -open class UserListStateProvider : PreviewParameterProvider { +open class UserListStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aUserListState(), @@ -68,7 +68,7 @@ fun aUserListState( showSearchLoader: Boolean = false, selectionMode: SelectionMode = SelectionMode.Single, recentDirectRooms: List = emptyList(), - eventSink: (UserListEvents) -> Unit = {}, + eventSink: (UserListEvent) -> Unit = {}, ) = UserListState( searchQuery = TextFieldState(initialText = searchQuery), isSearchActive = isSearchActive, diff --git a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt index 57f6cd2333..39d6f34b21 100644 --- a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt +++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatPresenterTest.kt @@ -53,7 +53,7 @@ class StartChatPresenterTest { assertThat(initialState.userListState.isSearchActive).isFalse() assertThat(initialState.userListState.isMultiSelectionEnabled).isFalse() val matrixUser = MatrixUser(UserId("@name:domain")) - initialState.eventSink(StartChatEvents.StartDM(matrixUser)) + initialState.eventSink(StartChatEvent.StartDM(matrixUser)) awaitItem().also { state -> assertThat(state.startDmAction).isEqualTo(startDMFailureResult) executeResult.assertions().isCalledOnce().with( @@ -61,7 +61,7 @@ class StartChatPresenterTest { value(false), any(), ) - state.eventSink(StartChatEvents.CancelStartDM) + state.eventSink(StartChatEvent.CancelStartDM) } awaitItem().also { state -> assertThat(state.startDmAction.isUninitialized()).isTrue() @@ -85,7 +85,7 @@ class StartChatPresenterTest { assertThat(initialState.userListState.isSearchActive).isFalse() assertThat(initialState.userListState.isMultiSelectionEnabled).isFalse() val matrixUser = MatrixUser(UserId("@name:domain")) - initialState.eventSink(StartChatEvents.StartDM(matrixUser)) + initialState.eventSink(StartChatEvent.StartDM(matrixUser)) awaitItem().also { state -> assertThat(state.startDmAction).isEqualTo(startDMSuccessResult) executeResult.assertions().isCalledOnce().with( @@ -109,7 +109,7 @@ class StartChatPresenterTest { presenter.test { val initialState = awaitItem() assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - initialState.eventSink(StartChatEvents.StartDM(matrixUser)) + initialState.eventSink(StartChatEvent.StartDM(matrixUser)) val confirmingState = awaitItem() assertThat(confirmingState.startDmAction).isEqualTo(startDMConfirmationResult) executeResult.assertions().isCalledOnce().with( @@ -118,7 +118,7 @@ class StartChatPresenterTest { any(), ) // Cancelling should not create the DM - confirmingState.eventSink(StartChatEvents.CancelStartDM) + confirmingState.eventSink(StartChatEvent.CancelStartDM) val finalState = awaitItem() assertThat(finalState.startDmAction.isUninitialized()).isTrue() executeResult.assertions().isCalledExactly(1) @@ -137,7 +137,7 @@ class StartChatPresenterTest { presenter.test { val initialState = awaitItem() assertThat(initialState.startDmAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - initialState.eventSink(StartChatEvents.StartDM(matrixUser)) + initialState.eventSink(StartChatEvent.StartDM(matrixUser)) val confirmingState = awaitItem() assertThat(confirmingState.startDmAction).isEqualTo(startDMConfirmationResult) executeResult.assertions().isCalledOnce().with( @@ -146,7 +146,7 @@ class StartChatPresenterTest { any(), ) // Start DM again should invoke the action with createIfDmDoesNotExist = true - confirmingState.eventSink(StartChatEvents.StartDM(matrixUser)) + confirmingState.eventSink(StartChatEvent.StartDM(matrixUser)) executeResult.assertions().isCalledExactly(2).withSequence( listOf(value(matrixUser), value(false), any()), listOf(value(matrixUser), value(true), any()), diff --git a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatViewTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatViewTest.kt index 8b5e1da8e6..7b7c046bb2 100644 --- a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatViewTest.kt +++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/root/StartChatViewTest.kt @@ -36,7 +36,7 @@ import org.robolectric.annotation.Config class StartChatViewTest : RobolectricTest() { @Test fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { setStartChatView( aCreateRoomRootState( @@ -50,7 +50,7 @@ class StartChatViewTest : RobolectricTest() { @Test fun `clicking on New room invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { setStartChatView( aCreateRoomRootState( @@ -65,7 +65,7 @@ class StartChatViewTest : RobolectricTest() { @Config(qualifiers = "h1024dp") @Test fun `clicking on Invite people invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { setStartChatView( aCreateRoomRootState( @@ -84,7 +84,7 @@ class StartChatViewTest : RobolectricTest() { fun `clicking on a user suggestion invokes the expected callback`() = runAndroidComposeUiTest { val recentDirectRoomList = aRecentDirectRoomList() val firstRoom = recentDirectRoomList[0] - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnceWithParam(firstRoom.roomId) { setStartChatView( aCreateRoomRootState( @@ -102,7 +102,7 @@ class StartChatViewTest : RobolectricTest() { @Config(qualifiers = "h1024dp") @Test fun `clicking on Join room by address invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { setStartChatView( aCreateRoomRootState( @@ -116,7 +116,7 @@ class StartChatViewTest : RobolectricTest() { @Test fun `clicking on room directory invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { setStartChatView( aCreateRoomRootState( diff --git a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenterTest.kt b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenterTest.kt index 55fd30a84e..0366ec79bd 100644 --- a/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenterTest.kt +++ b/features/startchat/impl/src/test/kotlin/io/element/android/features/startchat/impl/userlist/DefaultUserListPresenterTest.kt @@ -80,7 +80,7 @@ class DefaultUserListPresenterTest { presenter.test { val initialState = awaitItem() - initialState.eventSink(UserListEvents.OnSearchActiveChanged(true)) + initialState.eventSink(UserListEvent.OnSearchActiveChanged(true)) assertThat(awaitItem().isSearchActive).isTrue() val matrixIdQuery = "@name:matrix.org" @@ -93,7 +93,7 @@ class DefaultUserListPresenterTest { assertThat(awaitItem().searchQuery.text.toString()).isEqualTo(notMatrixIdQuery) assertThat(userRepository.providedQuery).isEqualTo(notMatrixIdQuery) - initialState.eventSink(UserListEvents.OnSearchActiveChanged(false)) + initialState.eventSink(UserListEvent.OnSearchActiveChanged(false)) assertThat(awaitItem().isSearchActive).isFalse() } } @@ -190,22 +190,22 @@ class DefaultUserListPresenterTest { val userABis = aMatrixUser("@userA:domain", "A") val userC = aMatrixUser("@userC:domain", "C") - initialState.eventSink(UserListEvents.AddToSelection(userA)) + initialState.eventSink(UserListEvent.AddToSelection(userA)) assertThat(awaitItem().selectedUsers).containsExactly(userA) - initialState.eventSink(UserListEvents.AddToSelection(userB)) + initialState.eventSink(UserListEvent.AddToSelection(userB)) assertThat(awaitItem().selectedUsers).containsExactly(userA, userB) - initialState.eventSink(UserListEvents.AddToSelection(userABis)) - initialState.eventSink(UserListEvents.AddToSelection(userC)) + initialState.eventSink(UserListEvent.AddToSelection(userABis)) + initialState.eventSink(UserListEvent.AddToSelection(userC)) // duplicated users should be ignored assertThat(awaitItem().selectedUsers).containsExactly(userA, userB, userC) - initialState.eventSink(UserListEvents.RemoveFromSelection(userB)) + initialState.eventSink(UserListEvent.RemoveFromSelection(userB)) assertThat(awaitItem().selectedUsers).containsExactly(userA, userC) - initialState.eventSink(UserListEvents.RemoveFromSelection(userA)) + initialState.eventSink(UserListEvent.RemoveFromSelection(userA)) assertThat(awaitItem().selectedUsers).containsExactly(userC) - initialState.eventSink(UserListEvents.RemoveFromSelection(userC)) + initialState.eventSink(UserListEvent.RemoveFromSelection(userC)) assertThat(awaitItem().selectedUsers).isEmpty() } } diff --git a/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileEvents.kt b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileEvent.kt similarity index 57% rename from features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileEvents.kt rename to features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileEvent.kt index 55c9c40f74..24265f0d9b 100644 --- a/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileEvents.kt +++ b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileEvent.kt @@ -8,13 +8,13 @@ package io.element.android.features.userprofile.api -sealed interface UserProfileEvents { - data object StartDM : UserProfileEvents - data object ClearStartDMState : UserProfileEvents - data class BlockUser(val needsConfirmation: Boolean = false) : UserProfileEvents - data class UnblockUser(val needsConfirmation: Boolean = false) : UserProfileEvents - data object ClearBlockUserError : UserProfileEvents - data object ClearConfirmationDialog : UserProfileEvents - data object WithdrawVerification : UserProfileEvents - data class CopyToClipboard(val text: String) : UserProfileEvents +sealed interface UserProfileEvent { + data object StartDM : UserProfileEvent + data object ClearStartDMState : UserProfileEvent + data class BlockUser(val needsConfirmation: Boolean = false) : UserProfileEvent + data class UnblockUser(val needsConfirmation: Boolean = false) : UserProfileEvent + data object ClearBlockUserError : UserProfileEvent + data object ClearConfirmationDialog : UserProfileEvent + data object WithdrawVerification : UserProfileEvent + data class CopyToClipboard(val text: String) : UserProfileEvent } diff --git a/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt index 4de0d50ca4..bdc1c5a5c7 100644 --- a/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt +++ b/features/userprofile/api/src/main/kotlin/io/element/android/features/userprofile/api/UserProfileState.kt @@ -28,7 +28,7 @@ data class UserProfileState( val canCall: Boolean, val snackbarMessage: SnackbarMessage?, val displayedStatus: DisplayedStatus?, - val eventSink: (UserProfileEvents) -> Unit + val eventSink: (UserProfileEvent) -> Unit ) { enum class ConfirmationDialog { Block, diff --git a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt index 24977713c0..08251981c9 100644 --- a/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt +++ b/features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/root/UserProfilePresenter.kt @@ -23,7 +23,7 @@ import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.startchat.api.StartDMAction -import io.element.android.features.userprofile.api.UserProfileEvents +import io.element.android.features.userprofile.api.UserProfileEvent import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.api.UserProfileState.ConfirmationDialog import io.element.android.features.userprofile.api.UserProfileVerificationState @@ -101,9 +101,9 @@ class UserProfilePresenter( } val userProfile by produceState(null) { value = client.getProfile(userId).getOrNull() } - fun handleEvent(event: UserProfileEvents) { + fun handleEvent(event: UserProfileEvent) { when (event) { - is UserProfileEvents.BlockUser -> { + is UserProfileEvent.BlockUser -> { if (event.needsConfirmation) { confirmationDialog = ConfirmationDialog.Block } else { @@ -111,7 +111,7 @@ class UserProfilePresenter( coroutineScope.blockUser(isBlocked) } } - is UserProfileEvents.UnblockUser -> { + is UserProfileEvent.UnblockUser -> { if (event.needsConfirmation) { confirmationDialog = ConfirmationDialog.Unblock } else { @@ -119,11 +119,11 @@ class UserProfilePresenter( coroutineScope.unblockUser(isBlocked) } } - UserProfileEvents.ClearConfirmationDialog -> confirmationDialog = null - UserProfileEvents.ClearBlockUserError -> { + UserProfileEvent.ClearConfirmationDialog -> confirmationDialog = null + UserProfileEvent.ClearBlockUserError -> { isBlocked.value = AsyncData.Success(isBlocked.value.dataOrNull().orFalse()) } - UserProfileEvents.StartDM -> { + UserProfileEvent.StartDM -> { coroutineScope.launch { startDMAction.execute( matrixUser = userProfile ?: MatrixUser(userId), @@ -132,12 +132,12 @@ class UserProfilePresenter( ) } } - UserProfileEvents.ClearStartDMState -> { + UserProfileEvent.ClearStartDMState -> { startDmActionState.value = AsyncAction.Uninitialized } // Do nothing for other event as they are handled by the RoomMemberDetailsPresenter if needed - UserProfileEvents.WithdrawVerification, - is UserProfileEvents.CopyToClipboard -> Unit + UserProfileEvent.WithdrawVerification, + is UserProfileEvent.CopyToClipboard -> Unit } } diff --git a/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt b/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt index bbdc698f17..57a7ae273f 100644 --- a/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt +++ b/features/userprofile/impl/src/test/kotlin/io/element/android/features/userprofile/impl/UserProfilePresenterTest.kt @@ -18,7 +18,7 @@ import io.element.android.features.enterprise.test.FakeSessionEnterpriseService import io.element.android.features.invitepeople.test.FakeStartDMAction import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser import io.element.android.features.startchat.api.StartDMAction -import io.element.android.features.userprofile.api.UserProfileEvents +import io.element.android.features.userprofile.api.UserProfileEvent import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.api.UserProfileVerificationState import io.element.android.features.userprofile.impl.root.UserProfilePresenter @@ -182,12 +182,12 @@ class UserProfilePresenterTest { val presenter = createUserProfilePresenter() presenter.test { val initialState = awaitFirstItem() - initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = true)) + initialState.eventSink(UserProfileEvent.BlockUser(needsConfirmation = true)) val dialogState = awaitItem() assertThat(dialogState.displayConfirmationDialog).isEqualTo(UserProfileState.ConfirmationDialog.Block) - dialogState.eventSink(UserProfileEvents.ClearConfirmationDialog) + dialogState.eventSink(UserProfileEvent.ClearConfirmationDialog) assertThat(awaitItem().displayConfirmationDialog).isNull() } } @@ -202,12 +202,12 @@ class UserProfilePresenterTest { ) presenter.test { val initialState = awaitFirstItem() - initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false)) + initialState.eventSink(UserProfileEvent.BlockUser(needsConfirmation = false)) assertThat(awaitItem().isBlocked.isLoading()).isTrue() ignoredUsersFlow.emit(persistentListOf(A_USER_ID)) assertThat(awaitItem().isBlocked.dataOrNull()).isTrue() - initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false)) + initialState.eventSink(UserProfileEvent.UnblockUser(needsConfirmation = false)) assertThat(awaitItem().isBlocked.isLoading()).isTrue() ignoredUsersFlow.emit(persistentListOf()) assertThat(awaitItem().isBlocked.dataOrNull()).isFalse() @@ -222,12 +222,12 @@ class UserProfilePresenterTest { val presenter = createUserProfilePresenter(client = matrixClient) presenter.test { val initialState = awaitFirstItem(count = 2) - initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false)) + initialState.eventSink(UserProfileEvent.BlockUser(needsConfirmation = false)) assertThat(awaitItem().isBlocked.isLoading()).isTrue() val errorState = awaitItem() assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(AN_EXCEPTION) // Clear error - initialState.eventSink(UserProfileEvents.ClearBlockUserError) + initialState.eventSink(UserProfileEvent.ClearBlockUserError) assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(false)) } } @@ -240,12 +240,12 @@ class UserProfilePresenterTest { val presenter = createUserProfilePresenter(client = matrixClient) presenter.test { val initialState = awaitFirstItem(count = 2) - initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false)) + initialState.eventSink(UserProfileEvent.UnblockUser(needsConfirmation = false)) assertThat(awaitItem().isBlocked.isLoading()).isTrue() val errorState = awaitItem() assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(AN_EXCEPTION) // Clear error - initialState.eventSink(UserProfileEvents.ClearBlockUserError) + initialState.eventSink(UserProfileEvent.ClearBlockUserError) assertThat(awaitItem().isBlocked).isEqualTo(AsyncData.Success(true)) } } @@ -255,12 +255,12 @@ class UserProfilePresenterTest { val presenter = createUserProfilePresenter() presenter.test { val initialState = awaitFirstItem() - initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = true)) + initialState.eventSink(UserProfileEvent.UnblockUser(needsConfirmation = true)) val dialogState = awaitItem() assertThat(dialogState.displayConfirmationDialog).isEqualTo(UserProfileState.ConfirmationDialog.Unblock) - dialogState.eventSink(UserProfileEvents.ClearConfirmationDialog) + dialogState.eventSink(UserProfileEvent.ClearConfirmationDialog) assertThat(awaitItem().displayConfirmationDialog).isNull() } } @@ -279,7 +279,7 @@ class UserProfilePresenterTest { val initialState = awaitFirstItem() assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java) val matrixUser = MatrixUser(UserId("@alice:server.org")) - initialState.eventSink(UserProfileEvents.StartDM) + initialState.eventSink(UserProfileEvent.StartDM) awaitItem().also { state -> assertThat(state.startDmActionState).isEqualTo(startDMFailureResult) executeResult.assertions().isCalledOnce().with( @@ -287,7 +287,7 @@ class UserProfilePresenterTest { value(false), any(), ) - state.eventSink(UserProfileEvents.ClearStartDMState) + state.eventSink(UserProfileEvent.ClearStartDMState) } awaitItem().also { state -> assertThat(state.startDmActionState.isUninitialized()).isTrue() @@ -309,7 +309,7 @@ class UserProfilePresenterTest { val initialState = awaitFirstItem() assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java) val matrixUser = MatrixUser(UserId("@alice:server.org")) - initialState.eventSink(UserProfileEvents.StartDM) + initialState.eventSink(UserProfileEvent.StartDM) awaitItem().also { state -> assertThat(state.startDmActionState).isEqualTo(startDMSuccessResult) executeResult.assertions().isCalledOnce().with( @@ -335,7 +335,7 @@ class UserProfilePresenterTest { }.test { val initialState = awaitFirstItem() assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java) - initialState.eventSink(UserProfileEvents.StartDM) + initialState.eventSink(UserProfileEvent.StartDM) val confirmingState = awaitItem() assertThat(confirmingState.startDmActionState).isEqualTo(startDMConfirmationResult) executeResult.assertions().isCalledOnce().with( @@ -344,7 +344,7 @@ class UserProfilePresenterTest { any(), ) // Cancelling should not create the DM - confirmingState.eventSink(UserProfileEvents.ClearStartDMState) + confirmingState.eventSink(UserProfileEvent.ClearStartDMState) val finalState = awaitItem() assertThat(finalState.startDmActionState.isUninitialized()).isTrue() executeResult.assertions().isCalledExactly(1) @@ -365,7 +365,7 @@ class UserProfilePresenterTest { }.test { val initialState = awaitFirstItem() assertThat(initialState.startDmActionState).isInstanceOf(AsyncAction.Uninitialized::class.java) - initialState.eventSink(UserProfileEvents.StartDM) + initialState.eventSink(UserProfileEvent.StartDM) val confirmingState = awaitItem() assertThat(confirmingState.startDmActionState).isEqualTo(startDMConfirmationResult) executeResult.assertions().isCalledOnce().with( @@ -374,7 +374,7 @@ class UserProfilePresenterTest { any(), ) // Start DM again should invoke the action with createIfDmDoesNotExist = true - confirmingState.eventSink(UserProfileEvents.StartDM) + confirmingState.eventSink(UserProfileEvent.StartDM) executeResult.assertions().isCalledExactly(2).withSequence( listOf(value(matrixUser), value(false), any()), listOf(value(matrixUser), value(true), any()), diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStatePreviewParam.kt similarity index 95% rename from features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt rename to features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStatePreviewParam.kt index 4e25284753..bfb3df4ac6 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStateProvider.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileStatePreviewParam.kt @@ -10,7 +10,7 @@ package io.element.android.features.userprofile.shared import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser -import io.element.android.features.userprofile.api.UserProfileEvents +import io.element.android.features.userprofile.api.UserProfileEvent import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.api.UserProfileVerificationState import io.element.android.libraries.architecture.AsyncAction @@ -21,7 +21,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.matrix.ui.components.aMatrixUser -open class UserProfileStateProvider : PreviewParameterProvider { +open class UserProfileStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aUserProfileState(), @@ -50,7 +50,7 @@ fun aUserProfileState( canCall: Boolean = false, snackbarMessage: SnackbarMessage? = null, displayedStatus: DisplayedStatus? = null, - eventSink: (UserProfileEvents) -> Unit = {}, + eventSink: (UserProfileEvent) -> Unit = {}, ) = UserProfileState( userId = userId, userName = userName, diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt index c5a661dbac..643db456b1 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/UserProfileView.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser -import io.element.android.features.userprofile.api.UserProfileEvents +import io.element.android.features.userprofile.api.UserProfileEvent import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.api.UserProfileVerificationState import io.element.android.features.userprofile.shared.blockuser.BlockUserDialogs @@ -83,15 +83,15 @@ fun UserProfileView( openAvatarPreview(state.userName ?: state.userId.value, avatarUrl) }, onUserIdClick = { - state.eventSink(UserProfileEvents.CopyToClipboard(state.userId.value)) + state.eventSink(UserProfileEvent.CopyToClipboard(state.userId.value)) }, - withdrawVerificationClick = { state.eventSink(UserProfileEvents.WithdrawVerification) }, + withdrawVerificationClick = { state.eventSink(UserProfileEvent.WithdrawVerification) }, ) UserProfileMainActionsSection( isCurrentUser = state.isCurrentUser, canCall = state.canCall, onShareUser = onShareUser, - onStartDM = { state.eventSink(UserProfileEvents.StartDM) }, + onStartDM = { state.eventSink(UserProfileEvent.StartDM) }, onCall = { intent -> state.dmRoomId?.let { onStartCall(it, intent) } } ) Spacer(modifier = Modifier.height(26.dp)) @@ -109,18 +109,18 @@ fun UserProfileView( }, onSuccess = onOpenDm, errorMessage = { stringResource(R.string.screen_start_chat_error_starting_chat) }, - onRetry = { state.eventSink(UserProfileEvents.StartDM) }, - onErrorDismiss = { state.eventSink(UserProfileEvents.ClearStartDMState) }, + onRetry = { state.eventSink(UserProfileEvent.StartDM) }, + onErrorDismiss = { state.eventSink(UserProfileEvent.ClearStartDMState) }, confirmationDialog = { data -> if (data is ConfirmingStartDmWithMatrixUser) { CreateDmConfirmationBottomSheet( matrixUser = data.matrixUser, isUserIdentityUnknown = data.isUserIdentityUnknown, onSendInvite = { - state.eventSink(UserProfileEvents.StartDM) + state.eventSink(UserProfileEvent.StartDM) }, onDismiss = { - state.eventSink(UserProfileEvents.ClearStartDMState) + state.eventSink(UserProfileEvent.ClearStartDMState) }, ) } @@ -147,7 +147,7 @@ private fun VerifyUserSection( @PreviewsDayNight @Composable internal fun UserProfileViewPreview( - @PreviewParameter(UserProfileStateProvider::class) state: UserProfileState + @PreviewParameter(UserProfileStatePreviewParam::class) state: UserProfileState ) = ElementPreview { UserProfileView( state = state, diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogs.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogs.kt index 2ba17b947b..220118c5a7 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogs.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogs.kt @@ -10,7 +10,7 @@ package io.element.android.features.userprofile.shared.blockuser import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource -import io.element.android.features.userprofile.api.UserProfileEvents +import io.element.android.features.userprofile.api.UserProfileEvent import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.shared.R import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog @@ -23,24 +23,24 @@ fun BlockUserDialogs(state: UserProfileState) { BlockConfirmationDialog( onBlockAction = { state.eventSink( - UserProfileEvents.BlockUser( + UserProfileEvent.BlockUser( needsConfirmation = false ) ) }, - onDismiss = { state.eventSink(UserProfileEvents.ClearConfirmationDialog) } + onDismiss = { state.eventSink(UserProfileEvent.ClearConfirmationDialog) } ) } UserProfileState.ConfirmationDialog.Unblock -> { UnblockConfirmationDialog( onUnblockAction = { state.eventSink( - UserProfileEvents.UnblockUser( + UserProfileEvent.UnblockUser( needsConfirmation = false ) ) }, - onDismiss = { state.eventSink(UserProfileEvents.ClearConfirmationDialog) } + onDismiss = { state.eventSink(UserProfileEvent.ClearConfirmationDialog) } ) } } diff --git a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserSection.kt b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserSection.kt index 30f8c28607..e8210997b0 100644 --- a/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserSection.kt +++ b/features/userprofile/shared/src/main/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserSection.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.userprofile.api.UserProfileEvents +import io.element.android.features.userprofile.api.UserProfileEvent import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.shared.R import io.element.android.libraries.architecture.AsyncData @@ -50,13 +50,13 @@ fun BlockUserSection( if (isBlocked is AsyncData.Failure) { RetryDialog( content = stringResource(CommonStrings.error_unknown), - onDismiss = { state.eventSink(UserProfileEvents.ClearBlockUserError) }, + onDismiss = { state.eventSink(UserProfileEvent.ClearBlockUserError) }, onRetry = { val event = when (isBlocked.prevData) { - true -> UserProfileEvents.UnblockUser(needsConfirmation = false) - false -> UserProfileEvents.BlockUser(needsConfirmation = false) + true -> UserProfileEvent.UnblockUser(needsConfirmation = false) + false -> UserProfileEvent.BlockUser(needsConfirmation = false) // null case Should not happen - null -> UserProfileEvents.ClearBlockUserError + null -> UserProfileEvent.ClearBlockUserError } state.eventSink(event) }, @@ -68,7 +68,7 @@ fun BlockUserSection( private fun PreferenceBlockUser( isBlocked: Boolean?, isLoading: Boolean, - eventSink: (UserProfileEvents) -> Unit, + eventSink: (UserProfileEvent) -> Unit, ) { val loadingCurrentValue = @Composable { _: Boolean -> CircularProgressIndicator( @@ -82,7 +82,7 @@ private fun PreferenceBlockUser( ListItem( content = { Text(stringResource(R.string.screen_dm_details_unblock_user)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())), - onClick = { if (!isLoading) eventSink(UserProfileEvents.UnblockUser(needsConfirmation = true)) }, + onClick = { if (!isLoading) eventSink(UserProfileEvent.UnblockUser(needsConfirmation = true)) }, trailingContent = if (isLoading) ListItemContent.Custom(loadingCurrentValue) else null, ) } else { @@ -90,7 +90,7 @@ private fun PreferenceBlockUser( content = { Text(stringResource(R.string.screen_dm_details_block_user)) }, leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Block())), style = ListItemStyle.Destructive, - onClick = { if (!isLoading) eventSink(UserProfileEvents.BlockUser(needsConfirmation = true)) }, + onClick = { if (!isLoading) eventSink(UserProfileEvent.BlockUser(needsConfirmation = true)) }, trailingContent = if (isLoading) ListItemContent.Custom(loadingCurrentValue) else null, ) } diff --git a/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt b/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt index 63930affee..9c6aa66670 100644 --- a/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt +++ b/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/UserProfileViewTest.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.v2.runAndroidComposeUiTest -import io.element.android.features.userprofile.api.UserProfileEvents +import io.element.android.features.userprofile.api.UserProfileEvent import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.api.UserProfileVerificationState import io.element.android.features.userprofile.shared.R @@ -89,7 +89,7 @@ class UserProfileViewTest : RobolectricTest() { @Test fun `on Message clicked - the StartDm event is emitted`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setUserProfileView( state = aUserProfileState( dmRoomId = A_ROOM_ID, @@ -97,7 +97,7 @@ class UserProfileViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_message) - eventsRecorder.assertSingle(UserProfileEvents.StartDM) + eventsRecorder.assertSingle(UserProfileEvent.StartDM) } @Test @@ -131,19 +131,19 @@ class UserProfileViewTest : RobolectricTest() { @Config(qualifiers = "h1024dp") @Test fun `on Block user clicked - a BlockUser event is emitted with needsConfirmation`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setUserProfileView( state = aUserProfileState( eventSink = eventsRecorder, ), ) clickOn(R.string.screen_dm_details_block_user) - eventsRecorder.assertSingle(UserProfileEvents.BlockUser(needsConfirmation = true)) + eventsRecorder.assertSingle(UserProfileEvent.BlockUser(needsConfirmation = true)) } @Test fun `on confirming block user - a BlockUser event is emitted without needsConfirmation`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setUserProfileView( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, @@ -151,12 +151,12 @@ class UserProfileViewTest : RobolectricTest() { ), ) clickOn(R.string.screen_dm_details_block_alert_action) - eventsRecorder.assertSingle(UserProfileEvents.BlockUser(needsConfirmation = false)) + eventsRecorder.assertSingle(UserProfileEvent.BlockUser(needsConfirmation = false)) } @Test fun `on canceling blocking a user - a ClearConfirmationDialog event is emitted`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setUserProfileView( state = aUserProfileState( displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, @@ -164,13 +164,13 @@ class UserProfileViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_cancel) - eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) + eventsRecorder.assertSingle(UserProfileEvent.ClearConfirmationDialog) } @Config(qualifiers = "h1024dp") @Test fun `on Unblock user clicked - an UnblockUser event is emitted with needsConfirmation`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setUserProfileView( state = aUserProfileState( isBlocked = AsyncData.Success(true), @@ -178,12 +178,12 @@ class UserProfileViewTest : RobolectricTest() { ), ) clickOn(R.string.screen_dm_details_unblock_user) - eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(needsConfirmation = true)) + eventsRecorder.assertSingle(UserProfileEvent.UnblockUser(needsConfirmation = true)) } @Test fun `on confirming Unblock user - an UnblockUser event is emitted without needsConfirmation`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setUserProfileView( state = aUserProfileState( isBlocked = AsyncData.Success(true), @@ -192,12 +192,12 @@ class UserProfileViewTest : RobolectricTest() { ), ) clickOn(R.string.screen_dm_details_unblock_alert_action) - eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(needsConfirmation = false)) + eventsRecorder.assertSingle(UserProfileEvent.UnblockUser(needsConfirmation = false)) } @Test fun `on canceling unblocking a user - a ClearConfirmationDialog event is emitted`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setUserProfileView( state = aUserProfileState( isBlocked = AsyncData.Success(true), @@ -206,7 +206,7 @@ class UserProfileViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_cancel) - eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) + eventsRecorder.assertSingle(UserProfileEvent.ClearConfirmationDialog) } @Test diff --git a/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogsTest.kt b/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogsTest.kt index b0a2590e8a..575c9ff7a0 100644 --- a/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogsTest.kt +++ b/features/userprofile/shared/src/test/kotlin/io/element/android/features/userprofile/shared/blockuser/BlockUserDialogsTest.kt @@ -12,7 +12,7 @@ package io.element.android.features.userprofile.shared.blockuser import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.v2.runAndroidComposeUiTest -import io.element.android.features.userprofile.api.UserProfileEvents +import io.element.android.features.userprofile.api.UserProfileEvent import io.element.android.features.userprofile.api.UserProfileState import io.element.android.features.userprofile.shared.R import io.element.android.features.userprofile.shared.aUserProfileState @@ -25,7 +25,7 @@ import org.junit.Test class BlockUserDialogsTest : RobolectricTest() { @Test fun `confirm block user emit expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setContent { BlockUserDialogs( state = aUserProfileState( @@ -35,12 +35,12 @@ class BlockUserDialogsTest : RobolectricTest() { ) } clickOn(R.string.screen_dm_details_block_alert_action) - eventsRecorder.assertSingle(UserProfileEvents.BlockUser(false)) + eventsRecorder.assertSingle(UserProfileEvent.BlockUser(false)) } @Test fun `cancel block user emit expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setContent { BlockUserDialogs( state = aUserProfileState( @@ -50,12 +50,12 @@ class BlockUserDialogsTest : RobolectricTest() { ) } clickOn(CommonStrings.action_cancel) - eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) + eventsRecorder.assertSingle(UserProfileEvent.ClearConfirmationDialog) } @Test fun `confirm unblock user emit expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setContent { BlockUserDialogs( state = aUserProfileState( @@ -65,12 +65,12 @@ class BlockUserDialogsTest : RobolectricTest() { ) } clickOn(R.string.screen_dm_details_unblock_alert_action) - eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(false)) + eventsRecorder.assertSingle(UserProfileEvent.UnblockUser(false)) } @Test fun `cancel unblock user emit expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setContent { BlockUserDialogs( state = aUserProfileState( @@ -80,6 +80,6 @@ class BlockUserDialogsTest : RobolectricTest() { ) } clickOn(CommonStrings.action_cancel) - eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) + eventsRecorder.assertSingle(UserProfileEvent.ClearConfirmationDialog) } } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt index aa991a54bc..4cde3f927f 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt @@ -108,18 +108,18 @@ class IncomingVerificationPresenter( } } - fun handleEvent(event: IncomingVerificationViewEvents) { + fun handleEvent(event: IncomingVerificationViewEvent) { Timber.d("Verification user action: ${event::class.simpleName}") when (event) { - IncomingVerificationViewEvents.StartVerification -> + IncomingVerificationViewEvent.StartVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.AcceptIncomingRequest) - IncomingVerificationViewEvents.IgnoreVerification -> + IncomingVerificationViewEvent.IgnoreVerification -> navigator.onFinish() - IncomingVerificationViewEvents.ConfirmVerification -> + IncomingVerificationViewEvent.ConfirmVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.AcceptChallenge) - IncomingVerificationViewEvents.DeclineVerification -> + IncomingVerificationViewEvent.DeclineVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge) - IncomingVerificationViewEvents.GoBack -> { + IncomingVerificationViewEvent.GoBack -> { when (val verificationStep = step) { is Step.Initial -> if (verificationStep.isWaiting) { stateAndDispatch.dispatchAction(StateMachineEvent.Cancel) diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt index 311fc13e74..e3663800b2 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt @@ -16,7 +16,7 @@ import io.element.android.libraries.matrix.api.verification.VerificationRequest data class IncomingVerificationState( val step: Step, val request: VerificationRequest.Incoming, - val eventSink: (IncomingVerificationViewEvents) -> Unit, + val eventSink: (IncomingVerificationViewEvent) -> Unit, ) { @Stable sealed interface Step { diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateProvider.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStatePreviewParam.kt similarity index 96% rename from features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateProvider.kt rename to features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStatePreviewParam.kt index 895f78cbdc..68672be025 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateProvider.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStatePreviewParam.kt @@ -20,7 +20,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails import io.element.android.libraries.matrix.api.verification.VerificationRequest -open class IncomingVerificationStateProvider : PreviewParameterProvider { +open class IncomingVerificationStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( anIncomingVerificationState(), @@ -87,7 +87,7 @@ internal fun anIncomingUserVerificationRequest() = VerificationRequest.Incoming. internal fun anIncomingVerificationState( step: Step = aStepInitial(), verificationRequest: VerificationRequest.Incoming = anIncomingSessionVerificationRequest(), - eventSink: (IncomingVerificationViewEvents) -> Unit = {}, + eventSink: (IncomingVerificationViewEvent) -> Unit = {}, ) = IncomingVerificationState( step = step, request = verificationRequest, diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt index e86c83825b..00b430fb53 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt @@ -62,7 +62,7 @@ fun IncomingVerificationView( val step = state.step BackHandler { - state.eventSink(IncomingVerificationViewEvents.GoBack) + state.eventSink(IncomingVerificationViewEvent.GoBack) } HeaderFooterPage( modifier = modifier, @@ -70,7 +70,7 @@ fun IncomingVerificationView( TopAppBar( title = {}, navigationIcon = { - BackButton(onClick = { state.eventSink(IncomingVerificationViewEvents.GoBack) }) + BackButton(onClick = { state.eventSink(IncomingVerificationViewEvent.GoBack) }) }, colors = topAppBarColors(containerColor = Color.Transparent), ) @@ -217,13 +217,13 @@ private fun IncomingVerificationBottomMenu( text = stringResource(CommonStrings.action_start_verification), enabled = !step.isWaiting, showProgress = step.isWaiting, - onClick = { eventSink(IncomingVerificationViewEvents.StartVerification) }, + onClick = { eventSink(IncomingVerificationViewEvent.StartVerification) }, ) TextButton( modifier = Modifier.fillMaxWidth(), text = stringResource(CommonStrings.action_ignore), enabled = !step.isWaiting, - onClick = { eventSink(IncomingVerificationViewEvents.IgnoreVerification) }, + onClick = { eventSink(IncomingVerificationViewEvent.IgnoreVerification) }, ) } } @@ -235,14 +235,14 @@ private fun IncomingVerificationBottomMenu( enabled = !step.isWaiting, showProgress = step.isWaiting, onClick = { - eventSink(IncomingVerificationViewEvents.ConfirmVerification) + eventSink(IncomingVerificationViewEvent.ConfirmVerification) }, ) TextButton( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.screen_session_verification_they_dont_match), enabled = !step.isWaiting, - onClick = { eventSink(IncomingVerificationViewEvents.DeclineVerification) }, + onClick = { eventSink(IncomingVerificationViewEvent.DeclineVerification) }, ) } } @@ -254,7 +254,7 @@ private fun IncomingVerificationBottomMenu( modifier = Modifier.fillMaxWidth(), text = stringResource(CommonStrings.action_done), onClick = { - eventSink(IncomingVerificationViewEvents.GoBack) + eventSink(IncomingVerificationViewEvent.GoBack) }, ) } @@ -264,7 +264,9 @@ private fun IncomingVerificationBottomMenu( @PreviewsDayNight @Composable -internal fun IncomingVerificationViewPreview(@PreviewParameter(IncomingVerificationStateProvider::class) state: IncomingVerificationState) = ElementPreview { +internal fun IncomingVerificationViewPreview(@PreviewParameter( + IncomingVerificationStatePreviewParam::class +) state: IncomingVerificationState) = ElementPreview { IncomingVerificationView( state = state, ) diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewEvents.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewEvent.kt similarity index 82% rename from features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewEvents.kt rename to features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewEvent.kt index 383354860b..26a05b44cc 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewEvents.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewEvent.kt @@ -8,10 +8,10 @@ package io.element.android.features.verifysession.impl.incoming -sealed interface IncomingVerificationViewEvents { - data object GoBack : IncomingVerificationViewEvents - data object StartVerification : IncomingVerificationViewEvents - data object IgnoreVerification : IncomingVerificationViewEvents - data object ConfirmVerification : IncomingVerificationViewEvents - data object DeclineVerification : IncomingVerificationViewEvents +sealed interface IncomingVerificationViewEvent { + data object GoBack : IncomingVerificationViewEvent + data object StartVerification : IncomingVerificationViewEvent + data object IgnoreVerification : IncomingVerificationViewEvent + data object ConfirmVerification : IncomingVerificationViewEvent + data object DeclineVerification : IncomingVerificationViewEvent } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenter.kt index 90e8a3e598..5ecc500725 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenter.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenter.kt @@ -93,16 +93,16 @@ class OutgoingVerificationPresenter( observeVerificationService() } - fun handleEvent(event: OutgoingVerificationViewEvents) { + fun handleEvent(event: OutgoingVerificationViewEvent) { Timber.d("Verification user action: ${event::class.simpleName}") when (event) { // Just relay the event to the state machine - OutgoingVerificationViewEvents.RequestVerification -> StateMachineEvent.RequestVerification(verificationRequest) - OutgoingVerificationViewEvents.StartSasVerification -> StateMachineEvent.StartSasVerification - OutgoingVerificationViewEvents.ConfirmVerification -> StateMachineEvent.AcceptChallenge - OutgoingVerificationViewEvents.DeclineVerification -> StateMachineEvent.DeclineChallenge - OutgoingVerificationViewEvents.Cancel -> StateMachineEvent.Cancel - OutgoingVerificationViewEvents.Reset -> StateMachineEvent.Reset + OutgoingVerificationViewEvent.RequestVerification -> StateMachineEvent.RequestVerification(verificationRequest) + OutgoingVerificationViewEvent.StartSasVerification -> StateMachineEvent.StartSasVerification + OutgoingVerificationViewEvent.ConfirmVerification -> StateMachineEvent.AcceptChallenge + OutgoingVerificationViewEvent.DeclineVerification -> StateMachineEvent.DeclineChallenge + OutgoingVerificationViewEvent.Cancel -> StateMachineEvent.Cancel + OutgoingVerificationViewEvent.Reset -> StateMachineEvent.Reset }.let { stateMachineEvent -> stateAndDispatch.dispatchAction(stateMachineEvent) } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationState.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationState.kt index 6ba8a03a7f..5f1d7aa921 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationState.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationState.kt @@ -16,7 +16,7 @@ import io.element.android.libraries.matrix.api.verification.VerificationRequest data class OutgoingVerificationState( val step: Step, val request: VerificationRequest.Outgoing, - val eventSink: (OutgoingVerificationViewEvents) -> Unit, + val eventSink: (OutgoingVerificationViewEvent) -> Unit, ) { @Stable sealed interface Step { diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationStateProvider.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationStatePreviewParam.kt similarity index 95% rename from features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationStateProvider.kt rename to features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationStatePreviewParam.kt index 2da099df9f..50abe14e9f 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationStateProvider.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationStatePreviewParam.kt @@ -16,7 +16,7 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.verification.VerificationRequest -open class OutgoingVerificationStateProvider : PreviewParameterProvider { +open class OutgoingVerificationStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( anOutgoingVerificationState( @@ -79,7 +79,7 @@ internal fun anOutgoingSessionVerificationRequest() = VerificationRequest.Outgoi internal fun anOutgoingVerificationState( step: Step = Step.Initial, request: VerificationRequest.Outgoing = anOutgoingSessionVerificationRequest(), - eventSink: (OutgoingVerificationViewEvents) -> Unit = {}, + eventSink: (OutgoingVerificationViewEvent) -> Unit = {}, ) = OutgoingVerificationState( step = step, request = request, diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationView.kt index 9131ad63fb..c9dc4ef45c 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationView.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationView.kt @@ -65,13 +65,13 @@ fun OutgoingVerificationView( val step = state.step fun cancelOrResetFlow() { when (step) { - is Step.Canceled -> state.eventSink(OutgoingVerificationViewEvents.Reset) + is Step.Canceled -> state.eventSink(OutgoingVerificationViewEvent.Reset) Step.Initial -> onBack() Step.Completed -> onFinish() - Step.Ready, is Step.AwaitingOtherDeviceResponse -> state.eventSink(OutgoingVerificationViewEvents.Cancel) + Step.Ready, is Step.AwaitingOtherDeviceResponse -> state.eventSink(OutgoingVerificationViewEvent.Cancel) is Step.Verifying -> { if (!step.state.isLoading()) { - state.eventSink(OutgoingVerificationViewEvents.DeclineVerification) + state.eventSink(OutgoingVerificationViewEvent.DeclineVerification) } } else -> Unit @@ -261,7 +261,7 @@ private fun OutgoingVerificationBottomMenu( text = stringResource(CommonStrings.action_start_verification), enabled = !isWaiting, showProgress = isWaiting, - onClick = { eventSink(OutgoingVerificationViewEvents.RequestVerification) }, + onClick = { eventSink(OutgoingVerificationViewEvent.RequestVerification) }, ) InvisibleButton() } @@ -281,7 +281,7 @@ private fun OutgoingVerificationBottomMenu( Button( modifier = Modifier.fillMaxWidth(), text = stringResource(CommonStrings.action_start), - onClick = { eventSink(OutgoingVerificationViewEvents.StartSasVerification) }, + onClick = { eventSink(OutgoingVerificationViewEvent.StartSasVerification) }, ) TextButton( modifier = Modifier.fillMaxWidth(), @@ -299,7 +299,7 @@ private fun OutgoingVerificationBottomMenu( enabled = !isVerifying, showProgress = isVerifying, onClick = { - eventSink(OutgoingVerificationViewEvents.ConfirmVerification) + eventSink(OutgoingVerificationViewEvent.ConfirmVerification) }, ) TextButton( @@ -307,7 +307,7 @@ private fun OutgoingVerificationBottomMenu( text = stringResource(R.string.screen_session_verification_they_dont_match), enabled = !isVerifying, onClick = { - eventSink(OutgoingVerificationViewEvents.DeclineVerification) + eventSink(OutgoingVerificationViewEvent.DeclineVerification) }, ) } @@ -328,7 +328,9 @@ private fun OutgoingVerificationBottomMenu( @PreviewsDayNight @Composable -internal fun OutgoingVerificationViewPreview(@PreviewParameter(OutgoingVerificationStateProvider::class) state: OutgoingVerificationState) = ElementPreview { +internal fun OutgoingVerificationViewPreview(@PreviewParameter( + OutgoingVerificationStatePreviewParam::class +) state: OutgoingVerificationState) = ElementPreview { OutgoingVerificationView( state = state, onLearnMoreClick = {}, diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewEvents.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewEvent.kt similarity index 75% rename from features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewEvents.kt rename to features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewEvent.kt index 9faab7e790..2edfa21b0c 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewEvents.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewEvent.kt @@ -8,11 +8,11 @@ package io.element.android.features.verifysession.impl.outgoing -sealed interface OutgoingVerificationViewEvents { - data object RequestVerification : OutgoingVerificationViewEvents - data object StartSasVerification : OutgoingVerificationViewEvents - data object ConfirmVerification : OutgoingVerificationViewEvents - data object DeclineVerification : OutgoingVerificationViewEvents - data object Cancel : OutgoingVerificationViewEvents - data object Reset : OutgoingVerificationViewEvents +sealed interface OutgoingVerificationViewEvent { + data object RequestVerification : OutgoingVerificationViewEvent + data object StartSasVerification : OutgoingVerificationViewEvent + data object ConfirmVerification : OutgoingVerificationViewEvent + data object DeclineVerification : OutgoingVerificationViewEvent + data object Cancel : OutgoingVerificationViewEvent + data object Reset : OutgoingVerificationViewEvent } diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt index a8d3802268..45d5c623c4 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt @@ -72,7 +72,7 @@ class IncomingVerificationPresenterTest { acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(anIncomingSessionVerificationRequest)) acceptVerificationRequestLambda.assertions().isNeverCalled() // User accept the incoming verification - initialState.eventSink(IncomingVerificationViewEvents.StartVerification) + initialState.eventSink(IncomingVerificationViewEvent.StartVerification) skipItems(1) val initialWaitingState = awaitItem() assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue() @@ -96,7 +96,7 @@ class IncomingVerificationPresenterTest { ) ) // User claims that the emoji matches - emojiState.eventSink(IncomingVerificationViewEvents.ConfirmVerification) + emojiState.eventSink(IncomingVerificationViewEvent.ConfirmVerification) val emojiWaitingItem = awaitItem() assertThat((emojiWaitingItem.step as IncomingVerificationState.Step.Verifying).isWaiting).isTrue() advanceUntilIdle() @@ -141,7 +141,7 @@ class IncomingVerificationPresenterTest { acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(anIncomingSessionVerificationRequest)) acceptVerificationRequestLambda.assertions().isNeverCalled() // User accept the incoming verification - initialState.eventSink(IncomingVerificationViewEvents.StartVerification) + initialState.eventSink(IncomingVerificationViewEvent.StartVerification) skipItems(1) val initialWaitingState = awaitItem() assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue() @@ -159,7 +159,7 @@ class IncomingVerificationPresenterTest { ) val emojiState = awaitItem() // User claims that the emojis do not match - emojiState.eventSink(IncomingVerificationViewEvents.DeclineVerification) + emojiState.eventSink(IncomingVerificationViewEvent.DeclineVerification) val emojiWaitingItem = awaitItem() assertThat((emojiWaitingItem.step as IncomingVerificationState.Step.Verifying).isWaiting).isTrue() advanceUntilIdle() @@ -241,7 +241,7 @@ class IncomingVerificationPresenterTest { acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(anIncomingSessionVerificationRequest)) acceptVerificationRequestLambda.assertions().isNeverCalled() // User accept the incoming verification - initialState.eventSink(IncomingVerificationViewEvents.StartVerification) + initialState.eventSink(IncomingVerificationViewEvent.StartVerification) skipItems(1) val initialWaitingState = awaitItem() assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue() @@ -259,7 +259,7 @@ class IncomingVerificationPresenterTest { ) val emojiState = awaitItem() // User goes back - emojiState.eventSink(IncomingVerificationViewEvents.GoBack) + emojiState.eventSink(IncomingVerificationViewEvent.GoBack) val emojiWaitingItem = awaitItem() assertThat((emojiWaitingItem.step as IncomingVerificationState.Step.Verifying).isWaiting).isTrue() advanceUntilIdle() @@ -289,7 +289,7 @@ class IncomingVerificationPresenterTest { navigator = IncomingVerificationNavigator(navigatorLambda), ).test { val initialState = awaitItem() - initialState.eventSink(IncomingVerificationViewEvents.IgnoreVerification) + initialState.eventSink(IncomingVerificationViewEvent.IgnoreVerification) skipItems(1) navigatorLambda.assertions().isCalledOnce() } diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt index 5947d1d0a5..c5c64585bc 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt @@ -27,7 +27,7 @@ class IncomingVerificationViewTest : RobolectricTest() { // region step Initial @Test fun `back key pressed - ignore the verification`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setIncomingVerificationView( anIncomingVerificationState( step = aStepInitial(), @@ -35,12 +35,12 @@ class IncomingVerificationViewTest : RobolectricTest() { ), ) pressBackKey() - eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) + eventsRecorder.assertSingle(IncomingVerificationViewEvent.GoBack) } @Test fun `ignore incoming verification emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setIncomingVerificationView( anIncomingVerificationState( step = aStepInitial(), @@ -48,12 +48,12 @@ class IncomingVerificationViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_ignore) - eventsRecorder.assertSingle(IncomingVerificationViewEvents.IgnoreVerification) + eventsRecorder.assertSingle(IncomingVerificationViewEvent.IgnoreVerification) } @Test fun `start incoming verification emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setIncomingVerificationView( anIncomingVerificationState( step = aStepInitial(), @@ -61,12 +61,12 @@ class IncomingVerificationViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_start_verification) - eventsRecorder.assertSingle(IncomingVerificationViewEvents.StartVerification) + eventsRecorder.assertSingle(IncomingVerificationViewEvent.StartVerification) } @Test fun `back key pressed - when awaiting response cancels the verification`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setIncomingVerificationView( anIncomingVerificationState( step = aStepInitial( @@ -76,14 +76,14 @@ class IncomingVerificationViewTest : RobolectricTest() { ), ) pressBackKey() - eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) + eventsRecorder.assertSingle(IncomingVerificationViewEvent.GoBack) } // endregion step Initial // region step Verifying @Test fun `back key pressed - when ready to verify cancels the verification`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Verifying( @@ -94,12 +94,12 @@ class IncomingVerificationViewTest : RobolectricTest() { ), ) pressBackKey() - eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) + eventsRecorder.assertSingle(IncomingVerificationViewEvent.GoBack) } @Test fun `back key pressed - when verifying and loading emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Verifying( @@ -110,12 +110,12 @@ class IncomingVerificationViewTest : RobolectricTest() { ), ) pressBackKey() - eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) + eventsRecorder.assertSingle(IncomingVerificationViewEvent.GoBack) } @Test fun `clicking on they do not match emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Verifying( @@ -126,12 +126,12 @@ class IncomingVerificationViewTest : RobolectricTest() { ), ) clickOn(R.string.screen_session_verification_they_dont_match) - eventsRecorder.assertSingle(IncomingVerificationViewEvents.DeclineVerification) + eventsRecorder.assertSingle(IncomingVerificationViewEvent.DeclineVerification) } @Test fun `clicking on they match emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Verifying( @@ -142,14 +142,14 @@ class IncomingVerificationViewTest : RobolectricTest() { ), ) clickOn(R.string.screen_session_verification_they_match) - eventsRecorder.assertSingle(IncomingVerificationViewEvents.ConfirmVerification) + eventsRecorder.assertSingle(IncomingVerificationViewEvent.ConfirmVerification) } // endregion // region step Failure @Test fun `back key pressed - when failure resets the flow`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Failure, @@ -157,12 +157,12 @@ class IncomingVerificationViewTest : RobolectricTest() { ), ) pressBackKey() - eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) + eventsRecorder.assertSingle(IncomingVerificationViewEvent.GoBack) } @Test fun `click on done - when failure resets the flow`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Failure, @@ -170,7 +170,7 @@ class IncomingVerificationViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_done) - eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) + eventsRecorder.assertSingle(IncomingVerificationViewEvent.GoBack) } // endregion @@ -178,7 +178,7 @@ class IncomingVerificationViewTest : RobolectricTest() { // region step Completed @Test fun `back key pressed - on Completed step emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Completed, @@ -186,12 +186,12 @@ class IncomingVerificationViewTest : RobolectricTest() { ), ) pressBackKey() - eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) + eventsRecorder.assertSingle(IncomingVerificationViewEvent.GoBack) } @Test fun `when flow is completed and the user clicks on the done button, the expected event is emitted`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setIncomingVerificationView( anIncomingVerificationState( step = IncomingVerificationState.Step.Completed, @@ -199,7 +199,7 @@ class IncomingVerificationViewTest : RobolectricTest() { ), ) clickOn(CommonStrings.action_done) - eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack) + eventsRecorder.assertSingle(IncomingVerificationViewEvent.GoBack) } // endregion diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt index bc4a46dc44..a65437ba50 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationPresenterTest.kt @@ -104,7 +104,7 @@ class OutgoingVerificationPresenterTest { val initialState = awaitItem() assertThat(initialState.step).isEqualTo(Step.Initial) val eventSink = initialState.eventSink - eventSink(OutgoingVerificationViewEvents.Cancel) + eventSink(OutgoingVerificationViewEvent.Cancel) assertThat(awaitItem().step).isEqualTo(Step.Exit) } @@ -120,7 +120,7 @@ class OutgoingVerificationPresenterTest { val presenter = createOutgoingVerificationPresenter(service) presenter.test { val state = requestVerificationAndAwaitVerifyingState(service) - state.eventSink(OutgoingVerificationViewEvents.ConfirmVerification) + state.eventSink(OutgoingVerificationViewEvent.ConfirmVerification) // Cancelling assertThat(awaitItem().step).isInstanceOf(Step.Verifying::class.java) service.emitVerificationFlowState(VerificationFlowState.DidFail) @@ -136,7 +136,7 @@ class OutgoingVerificationPresenterTest { ) val presenter = createOutgoingVerificationPresenter(service) presenter.test { - awaitItem().eventSink(OutgoingVerificationViewEvents.RequestVerification) + awaitItem().eventSink(OutgoingVerificationViewEvent.RequestVerification) service.emitVerificationFlowState(VerificationFlowState.DidFail) assertThat(awaitItem().step).isInstanceOf(Step.AwaitingOtherDeviceResponse::class.java) assertThat(awaitItem().step).isEqualTo(Step.Canceled) @@ -173,7 +173,7 @@ class OutgoingVerificationPresenterTest { val presenter = createOutgoingVerificationPresenter(service) presenter.test { val state = requestVerificationAndAwaitVerifyingState(service) - state.eventSink(OutgoingVerificationViewEvents.Cancel) + state.eventSink(OutgoingVerificationViewEvent.Cancel) assertThat(awaitItem().step).isEqualTo(Step.Canceled) } } @@ -203,7 +203,7 @@ class OutgoingVerificationPresenterTest { val state = requestVerificationAndAwaitVerifyingState(service) service.emitVerificationFlowState(VerificationFlowState.DidCancel) assertThat(awaitItem().step).isEqualTo(Step.Canceled) - state.eventSink(OutgoingVerificationViewEvents.Reset) + state.eventSink(OutgoingVerificationViewEvent.Reset) // Went back to initial state assertThat(awaitItem().step).isEqualTo(Step.Initial) cancelAndIgnoreRemainingEvents() @@ -226,7 +226,7 @@ class OutgoingVerificationPresenterTest { service, SessionVerificationData.Emojis(emojis) ) - state.eventSink(OutgoingVerificationViewEvents.ConfirmVerification) + state.eventSink(OutgoingVerificationViewEvent.ConfirmVerification) assertThat(awaitItem().step).isEqualTo( Step.Verifying( SessionVerificationData.Emojis(emojis), @@ -257,7 +257,7 @@ class OutgoingVerificationPresenterTest { ) presenter.test { val state = requestVerificationAndAwaitVerifyingState(service) - state.eventSink(OutgoingVerificationViewEvents.ConfirmVerification) + state.eventSink(OutgoingVerificationViewEvent.ConfirmVerification) assertThat(awaitItem().step).isInstanceOf(Step.Verifying::class.java) service.emitVerificationFlowState(VerificationFlowState.DidFinish) assertThat(awaitItem().step).isEqualTo(Step.Completed) @@ -283,7 +283,7 @@ class OutgoingVerificationPresenterTest { ) presenter.test { val state = requestVerificationAndAwaitVerifyingState(service) - state.eventSink(OutgoingVerificationViewEvents.ConfirmVerification) + state.eventSink(OutgoingVerificationViewEvent.ConfirmVerification) assertThat(awaitItem().step).isInstanceOf(Step.Verifying::class.java) service.emitVerificationFlowState(VerificationFlowState.DidFinish) assertThat(awaitItem().step).isEqualTo(Step.Completed) @@ -308,7 +308,7 @@ class OutgoingVerificationPresenterTest { ) presenter.test { val state = requestVerificationAndAwaitVerifyingState(service) - state.eventSink(OutgoingVerificationViewEvents.ConfirmVerification) + state.eventSink(OutgoingVerificationViewEvent.ConfirmVerification) assertThat(awaitItem().step).isInstanceOf(Step.Verifying::class.java) service.emitVerificationFlowState(VerificationFlowState.DidFinish) // The recovery state never leaves UNKNOWN, so the 10 seconds timeout fires and the flow completes anyway @@ -328,7 +328,7 @@ class OutgoingVerificationPresenterTest { val presenter = createOutgoingVerificationPresenter(service) presenter.test { val state = requestVerificationAndAwaitVerifyingState(service) - state.eventSink(OutgoingVerificationViewEvents.DeclineVerification) + state.eventSink(OutgoingVerificationViewEvent.DeclineVerification) assertThat(awaitItem().step).isEqualTo( Step.Verifying( SessionVerificationData.Emojis(emptyList()), @@ -384,7 +384,7 @@ class OutgoingVerificationPresenterTest { ): OutgoingVerificationState { var state = awaitItem() assertThat(state.step).isEqualTo(Step.Initial) - state.eventSink(OutgoingVerificationViewEvents.RequestVerification) + state.eventSink(OutgoingVerificationViewEvent.RequestVerification) testScope.advanceTimeBy(1.minutes) // Await for other device response: fakeService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest) @@ -393,7 +393,7 @@ class OutgoingVerificationPresenterTest { // Await for the state to be Ready state = awaitItem() assertThat(state.step).isEqualTo(Step.Ready) - state.eventSink(OutgoingVerificationViewEvents.StartSasVerification) + state.eventSink(OutgoingVerificationViewEvent.StartSasVerification) testScope.advanceUntilIdle() // Await for other device response (again): fakeService.emitVerificationFlowState(VerificationFlowState.DidStartSasVerification) diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewTest.kt index 2b5ec63ced..a186c56f3f 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/OutgoingVerificationViewTest.kt @@ -29,7 +29,7 @@ import org.junit.Test class OutgoingVerificationViewTest : RobolectricTest() { @Test fun `back key pressed - when canceled resets the flow`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Canceled, @@ -37,12 +37,12 @@ class OutgoingVerificationViewTest : RobolectricTest() { ), ) pressBackKey() - eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Reset) + eventsRecorder.assertSingle(OutgoingVerificationViewEvent.Reset) } @Test fun `back key pressed - when awaiting response cancels the verification`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.AwaitingOtherDeviceResponse, @@ -50,12 +50,12 @@ class OutgoingVerificationViewTest : RobolectricTest() { ), ) pressBackKey() - eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Cancel) + eventsRecorder.assertSingle(OutgoingVerificationViewEvent.Cancel) } @Test fun `back key pressed - when ready to verify cancels the verification`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Ready, @@ -63,12 +63,12 @@ class OutgoingVerificationViewTest : RobolectricTest() { ), ) pressBackKey() - eventsRecorder.assertSingle(OutgoingVerificationViewEvents.Cancel) + eventsRecorder.assertSingle(OutgoingVerificationViewEvent.Cancel) } @Test fun `back key pressed - when verifying and not loading declines the verification`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Verifying( @@ -79,12 +79,12 @@ class OutgoingVerificationViewTest : RobolectricTest() { ), ) pressBackKey() - eventsRecorder.assertSingle(OutgoingVerificationViewEvents.DeclineVerification) + eventsRecorder.assertSingle(OutgoingVerificationViewEvent.DeclineVerification) } @Test fun `back key pressed - when verifying and loading does nothing`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Verifying( @@ -113,7 +113,7 @@ class OutgoingVerificationViewTest : RobolectricTest() { @Test fun `when flow is completed and the user clicks on the done button, the expected callback is invoked`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { callback -> setOutgoingVerificationView( anOutgoingVerificationState( @@ -128,7 +128,7 @@ class OutgoingVerificationViewTest : RobolectricTest() { @Test fun `clicking on they match emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Verifying( @@ -139,12 +139,12 @@ class OutgoingVerificationViewTest : RobolectricTest() { ), ) clickOn(R.string.screen_session_verification_they_match) - eventsRecorder.assertSingle(OutgoingVerificationViewEvents.ConfirmVerification) + eventsRecorder.assertSingle(OutgoingVerificationViewEvent.ConfirmVerification) } @Test fun `clicking on they do not match emits the expected event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setOutgoingVerificationView( anOutgoingVerificationState( step = OutgoingVerificationState.Step.Verifying( @@ -155,7 +155,7 @@ class OutgoingVerificationViewTest : RobolectricTest() { ), ) clickOn(R.string.screen_session_verification_they_dont_match) - eventsRecorder.assertSingle(OutgoingVerificationViewEvents.DeclineVerification) + eventsRecorder.assertSingle(OutgoingVerificationViewEvent.DeclineVerification) } private fun AndroidComposeUiTest.setOutgoingVerificationView( diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileEvents.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileEvent.kt similarity index 72% rename from features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileEvents.kt rename to features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileEvent.kt index 2cc4f3a601..2ff6faf4ef 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileEvents.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileEvent.kt @@ -8,7 +8,7 @@ package io.element.android.features.viewfolder.impl.file -sealed interface ViewFileEvents { - data object SaveOnDisk : ViewFileEvents - data object Share : ViewFileEvents +sealed interface ViewFileEvent { + data object SaveOnDisk : ViewFileEvent + data object Share : ViewFileEvent } diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFilePresenter.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFilePresenter.kt index dd294bd53c..571f3eb053 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFilePresenter.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFilePresenter.kt @@ -44,10 +44,10 @@ class ViewFilePresenter( val coroutineScope = rememberCoroutineScope() val colorationMode = remember { name.toColorationMode() } - fun handleEvent(event: ViewFileEvents) { + fun handleEvent(event: ViewFileEvent) { when (event) { - ViewFileEvents.Share -> coroutineScope.share(path) - ViewFileEvents.SaveOnDisk -> coroutineScope.save(path) + ViewFileEvent.Share -> coroutineScope.share(path) + ViewFileEvent.SaveOnDisk -> coroutineScope.save(path) } } diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileState.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileState.kt index 979e188b65..e4f9255a14 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileState.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileState.kt @@ -14,7 +14,7 @@ data class ViewFileState( val name: String, val lines: AsyncData>, val colorationMode: ColorationMode, - val eventSink: (ViewFileEvents) -> Unit, + val eventSink: (ViewFileEvent) -> Unit, ) enum class ColorationMode { diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileStateProvider.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileStatePreviewParam.kt similarity index 97% rename from features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileStateProvider.kt rename to features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileStatePreviewParam.kt index 1b20b5c947..8420cf8fd6 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileStateProvider.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileStatePreviewParam.kt @@ -11,7 +11,7 @@ package io.element.android.features.viewfolder.impl.file import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncData -open class ViewFileStateProvider : PreviewParameterProvider { +open class ViewFileStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aViewFileState(), diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileView.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileView.kt index 23f92fc9ba..3c4ce9674c 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileView.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/file/ViewFileView.kt @@ -48,7 +48,7 @@ fun ViewFileView( actions = { IconButton( onClick = { - state.eventSink(ViewFileEvents.Share) + state.eventSink(ViewFileEvent.Share) }, ) { Icon( @@ -58,7 +58,7 @@ fun ViewFileView( } IconButton( onClick = { - state.eventSink(ViewFileEvents.SaveOnDisk) + state.eventSink(ViewFileEvent.SaveOnDisk) }, ) { Icon( @@ -92,7 +92,7 @@ fun ViewFileView( @PreviewsDayNight @Composable -internal fun ViewFileViewPreview(@PreviewParameter(ViewFileStateProvider::class) state: ViewFileState) = ElementPreview { +internal fun ViewFileViewPreview(@PreviewParameter(ViewFileStatePreviewParam::class) state: ViewFileState) = ElementPreview { ViewFileView( state = state, onBackClick = {}, diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderStateProvider.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderStatePreviewParam.kt similarity index 92% rename from features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderStateProvider.kt rename to features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderStatePreviewParam.kt index 2debe4cbf8..0fad56fe40 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderStateProvider.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderStatePreviewParam.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.viewfolder.impl.model.Item import kotlinx.collections.immutable.toImmutableList -open class ViewFolderStateProvider : PreviewParameterProvider { +open class ViewFolderStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aViewFolderState(), diff --git a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderView.kt b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderView.kt index 86cd5302c8..89fb3da7f4 100644 --- a/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderView.kt +++ b/features/viewfolder/impl/src/main/kotlin/io/element/android/features/viewfolder/impl/folder/ViewFolderView.kt @@ -142,7 +142,7 @@ private fun ItemRow( @PreviewsDayNight @Composable -internal fun ViewFolderViewPreview(@PreviewParameter(ViewFolderStateProvider::class) state: ViewFolderState) = ElementPreview { +internal fun ViewFolderViewPreview(@PreviewParameter(ViewFolderStatePreviewParam::class) state: ViewFolderState) = ElementPreview { ViewFolderView( state = state, onNavigateTo = {}, diff --git a/features/viewfolder/impl/src/test/kotlin/io/element/android/features/viewfolder/test/file/ViewFilePresenterTest.kt b/features/viewfolder/impl/src/test/kotlin/io/element/android/features/viewfolder/test/file/ViewFilePresenterTest.kt index 1a031ade28..c3be8d84b9 100644 --- a/features/viewfolder/impl/src/test/kotlin/io/element/android/features/viewfolder/test/file/ViewFilePresenterTest.kt +++ b/features/viewfolder/impl/src/test/kotlin/io/element/android/features/viewfolder/test/file/ViewFilePresenterTest.kt @@ -16,7 +16,7 @@ import io.element.android.features.viewfolder.impl.file.ColorationMode import io.element.android.features.viewfolder.impl.file.FileContentReader import io.element.android.features.viewfolder.impl.file.FileSave import io.element.android.features.viewfolder.impl.file.FileShare -import io.element.android.features.viewfolder.impl.file.ViewFileEvents +import io.element.android.features.viewfolder.impl.file.ViewFileEvent import io.element.android.features.viewfolder.impl.file.ViewFilePresenter import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.test.AN_EXCEPTION @@ -86,7 +86,7 @@ class ViewFilePresenterTest { }.test { skipItems(1) val initialState = awaitItem() - initialState.eventSink(ViewFileEvents.Share) + initialState.eventSink(ViewFileEvent.Share) assertThat(fileShare.hasBeenCalled).isTrue() assertThat(fileSave.hasBeenCalled).isFalse() } @@ -120,7 +120,7 @@ class ViewFilePresenterTest { }.test { skipItems(1) val initialState = awaitItem() - initialState.eventSink(ViewFileEvents.SaveOnDisk) + initialState.eventSink(ViewFileEvent.SaveOnDisk) assertThat(fileShare.hasBeenCalled).isFalse() assertThat(fileSave.hasBeenCalled).isTrue() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9c834fd291..46890d253f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,12 +17,12 @@ constraintlayout = "2.2.2" constraintlayout_compose = "1.1.2" lifecycle = "2.11.0" activity = "1.13.0" -media3 = "1.10.1" +media3 = "1.11.0" camera = "1.6.1" work = "2.11.2" # Compose -compose_bom = "2026.06.01" +compose_bom = "2026.08.00" # Coroutines coroutines = "1.11.0" @@ -32,7 +32,7 @@ accompanist = "0.37.3" # Test test_core = "1.7.0" -roborazzi = "1.71.0" +roborazzi = "1.72.0" # Jetbrain datetime = "0.8.0" @@ -54,7 +54,7 @@ haze = "1.7.2" dependencyAnalysis = "3.18.0" # DI -metro = "1.4.1" +metro = "1.4.2" # Auto service autoservice = "1.1.1" @@ -123,7 +123,7 @@ androidx_activity_activity = { module = "androidx.activity:activity", version.re androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" } androidx_startup = "androidx.startup:startup-runtime:1.2.0" androidx_preference = "androidx.preference:preference:1.2.1" -androidx_webkit = "androidx.webkit:webkit:1.16.0" +androidx_webkit = "androidx.webkit:webkit:1.17.0" androidx_compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_bom" } androidx_compose_material3 = { module = "androidx.compose.material3:material3", version = '1.5.0-alpha23' } @@ -149,7 +149,7 @@ accompanist_permission = { module = "com.google.accompanist:accompanist-permissi squareup_seismic = "com.squareup:seismic:1.0.3" # network -network_okhttp_bom = "com.squareup.okhttp3:okhttp-bom:5.4.0" +network_okhttp_bom = "com.squareup.okhttp3:okhttp-bom:5.5.0" network_okhttp_logging = { module = "com.squareup.okhttp3:logging-interceptor" } network_okhttp_okhttp = { module = "com.squareup.okhttp3:okhttp" } network_okhttp = { module = "com.squareup.okhttp3:okhttp" } @@ -185,7 +185,7 @@ test_detekt_test = { module = "io.gitlab.arturbosch.detekt:detekt-test", version # https://github.com/matrix-org/matrix-rust-components-kotlin/commits/main/sdk/sdk-android/src/main/kotlin/org/matrix/rustcomponents/sdk/matrix_sdk_ffi.kt # All new features should not be implemented in the pull request that upgrades the version, developers should # only fix API breaks and may add some TODOs. -matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.08.13" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:26.08.19" # Others coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } @@ -215,14 +215,14 @@ vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0" telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil3", version.ref = "telephoto" } telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" } statemachine = "com.freeletics.flowredux:compose:1.2.2" -maplibre = "org.maplibre.gl:android-sdk:13.4.1" +maplibre = "org.maplibre.gl:android-sdk:13.5.0" maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2" maplibre_compose = "org.maplibre.compose:maplibre-compose:0.14.0" maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2" opusencoder = "io.element.android:opusencoder:1.2.0" zxing_cpp = "io.github.zxing-cpp:android:3.1.1" google_zxing = "com.google.zxing:core:3.5.4" -google_guava = "com.google.guava:guava:33.6.0-android" +google_guava = "com.google.guava:guava:33.7.0-android" haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } haze_materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" } @@ -230,7 +230,7 @@ color_picker = "io.mhssn:colorpicker:1.0.0" # Analytics posthog = "com.posthog:posthog-android:3.56.4" -sentry = "io.sentry:sentry-android:8.52.0" +sentry = "io.sentry:sentry-android:8.53.0" # main branch can be tested replacing the version with main-SNAPSHOT matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.37.0" @@ -242,7 +242,7 @@ sigpwned_emoji4j = "com.sigpwned:emoji4j-core:16.0.0" metro_runtime = { module = "dev.zacsweers.metro:runtime", version.ref = "metro" } # Element Call -element_call_embedded = "io.element.android:element-call-embedded:0.23.0" +element_call_embedded = "io.element.android:element-call-embedded:0.24.0" # Auto services google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 84c5a9c661..760c45969c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=84fbba45c7f4c64abc77460e1c00f541e9f960e3c7ed2538f1ede19eacd873ae -distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip +distributionSha256Sum=acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectStateProvider.kt b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectStatePreviewParam.kt similarity index 90% rename from libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectStateProvider.kt rename to libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectStatePreviewParam.kt index 0c12d34352..78c2f895dd 100644 --- a/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectStateProvider.kt +++ b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectStatePreviewParam.kt @@ -13,7 +13,7 @@ import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.ui.components.aMatrixUserList import kotlinx.collections.immutable.toImmutableList -open class AccountSelectStateProvider : PreviewParameterProvider { +open class AccountSelectStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( anAccountSelectState(), diff --git a/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectView.kt b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectView.kt index bfa1bf45b6..74f15f8cfa 100644 --- a/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectView.kt +++ b/libraries/accountselect/impl/src/main/kotlin/io/element/android/libraries/accountselect/impl/AccountSelectView.kt @@ -80,7 +80,7 @@ fun AccountSelectView( @PreviewsDayNight @Composable -internal fun AccountSelectViewPreview(@PreviewParameter(AccountSelectStateProvider::class) state: AccountSelectState) = ElementPreview { +internal fun AccountSelectViewPreview(@PreviewParameter(AccountSelectStatePreviewParam::class) state: AccountSelectState) = ElementPreview { AccountSelectView( state = state, onSelectAccount = {}, diff --git a/libraries/accountselect/test/build.gradle.kts b/libraries/accountselect/test/build.gradle.kts new file mode 100644 index 0000000000..c38cd8ac69 --- /dev/null +++ b/libraries/accountselect/test/build.gradle.kts @@ -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. + */ +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.accountselect.test" +} + +dependencies { + implementation(projects.libraries.architecture) + implementation(projects.libraries.accountselect.api) + implementation(projects.tests.testutils) +} diff --git a/libraries/accountselect/test/src/main/kotlin/io/element/android/libraries/accountselect/test/FakeAccountSelectEntryPoint.kt b/libraries/accountselect/test/src/main/kotlin/io/element/android/libraries/accountselect/test/FakeAccountSelectEntryPoint.kt new file mode 100644 index 0000000000..acc5ba10df --- /dev/null +++ b/libraries/accountselect/test/src/main/kotlin/io/element/android/libraries/accountselect/test/FakeAccountSelectEntryPoint.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.libraries.accountselect.test + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeAccountSelectEntryPoint( + private val createNodeResult: (BuildContext, AccountSelectEntryPoint.Callback) -> Node = { _, _ -> lambdaError() }, +) : AccountSelectEntryPoint { + override fun createNode( + parentNode: Node, + buildContext: BuildContext, + callback: AccountSelectEntryPoint.Callback, + ): Node = createNodeResult(buildContext, callback) +} diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/media/PictureInPictureParamsExt.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/media/PictureInPictureParamsExt.kt new file mode 100644 index 0000000000..b17ec0260b --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/media/PictureInPictureParamsExt.kt @@ -0,0 +1,29 @@ +/* + * 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.libraries.androidutils.media + +import android.app.PictureInPictureParams +import android.content.res.Configuration +import android.os.Build +import android.util.Rational +import androidx.annotation.RequiresApi + +/** + * Set the aspect ratio of the Picture-in-Picture mode based on the current orientation of the other user's device. + */ +@RequiresApi(Build.VERSION_CODES.O) +fun PictureInPictureParams.Builder.setAspectRatioFromOrientation(orientation: Int): PictureInPictureParams.Builder { + val aspectRatio = if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + // If landscape orientation, invert the aspect ratio + Rational(5, 3) + } else { + // In any other case, assume portrait orientation + Rational(3, 5) + } + return setAspectRatio(aspectRatio) +} diff --git a/libraries/audio/impl/build.gradle.kts b/libraries/audio/impl/build.gradle.kts index 419e81962c..c0db3c4f86 100644 --- a/libraries/audio/impl/build.gradle.kts +++ b/libraries/audio/impl/build.gradle.kts @@ -1,4 +1,5 @@ import extension.setupDependencyInjection +import extension.testCommonDependencies /* * Copyright (c) 2025 Element Creations Ltd. @@ -22,4 +23,6 @@ dependencies { implementation(libs.androidx.corektx) implementation(projects.libraries.di) + + testCommonDependencies(libs) } diff --git a/libraries/audio/impl/src/main/kotlin/io/element/android/libraries/audio/impl/DefaultAudioFocus.kt b/libraries/audio/impl/src/main/kotlin/io/element/android/libraries/audio/impl/DefaultAudioFocus.kt index bc6a42d2f9..a9bc6fdbfd 100644 --- a/libraries/audio/impl/src/main/kotlin/io/element/android/libraries/audio/impl/DefaultAudioFocus.kt +++ b/libraries/audio/impl/src/main/kotlin/io/element/android/libraries/audio/impl/DefaultAudioFocus.kt @@ -48,9 +48,8 @@ class DefaultAudioFocus( } AudioManager.AUDIOFOCUS_LOSS_TRANSIENT, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { - // For recording, ignore transient focus losses (e.g., notification sounds). - // The AudioRecord API keeps capturing regardless. - if (requester != AudioFocusRequester.RecordVoiceMessage) { + Timber.d("AudioFocus: transient loss ($it)") + if (requester.pausesOnTransientFocusLoss()) { onFocusLost() } } @@ -96,8 +95,8 @@ class DefaultAudioFocus( private fun AudioFocusRequester.toAudioUsage(): Int { return when (this) { AudioFocusRequester.ElementCall, - AudioFocusRequester.VoiceMessage, AudioFocusRequester.RecordVoiceMessage -> AudioAttributes.USAGE_VOICE_COMMUNICATION + AudioFocusRequester.VoiceMessage, AudioFocusRequester.MediaViewer -> AudioAttributes.USAGE_MEDIA } } @@ -105,12 +104,22 @@ private fun AudioFocusRequester.toAudioUsage(): Int { private fun AudioFocusRequester.toAudioStream(): Int { return when (this) { AudioFocusRequester.ElementCall, - AudioFocusRequester.VoiceMessage, AudioFocusRequester.RecordVoiceMessage -> AudioManager.STREAM_VOICE_CALL + AudioFocusRequester.VoiceMessage, AudioFocusRequester.MediaViewer -> AudioManager.STREAM_MUSIC } } +private fun AudioFocusRequester.pausesOnTransientFocusLoss(): Boolean { + return when (this) { + // The AudioRecord API keeps capturing regardless. + AudioFocusRequester.RecordVoiceMessage, + AudioFocusRequester.VoiceMessage -> false + AudioFocusRequester.ElementCall, + AudioFocusRequester.MediaViewer -> true + } +} + private fun AudioFocusRequester.willPausedWhenDucked(): Boolean { return when (this) { // (note that for Element Call, there is no action when the focus is lost) diff --git a/libraries/audio/impl/src/test/kotlin/io/element/android/libraries/audio/impl/DefaultAudioFocusTest.kt b/libraries/audio/impl/src/test/kotlin/io/element/android/libraries/audio/impl/DefaultAudioFocusTest.kt new file mode 100644 index 0000000000..d0e255dd41 --- /dev/null +++ b/libraries/audio/impl/src/test/kotlin/io/element/android/libraries/audio/impl/DefaultAudioFocusTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * Copyright 2026 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.libraries.audio.impl + +import android.media.AudioAttributes +import android.media.AudioManager +import androidx.core.content.getSystemService +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.audio.api.AudioFocusRequester +import io.element.android.tests.testutils.robolectric.RobolectricTest +import org.junit.Test +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows.shadowOf + +class DefaultAudioFocusTest : RobolectricTest() { + @Test + fun `voice message playback requests focus for media, not for a call`() { + assertThat(usageOf(AudioFocusRequester.VoiceMessage)).isEqualTo(AudioAttributes.USAGE_MEDIA) + } + + @Test + fun `media viewer playback requests focus for media`() { + assertThat(usageOf(AudioFocusRequester.MediaViewer)).isEqualTo(AudioAttributes.USAGE_MEDIA) + } + + @Test + fun `voice message recording requests focus for voice communication`() { + assertThat(usageOf(AudioFocusRequester.RecordVoiceMessage)).isEqualTo(AudioAttributes.USAGE_VOICE_COMMUNICATION) + } + + @Test + fun `element call requests focus for voice communication`() { + assertThat(usageOf(AudioFocusRequester.ElementCall)).isEqualTo(AudioAttributes.USAGE_VOICE_COMMUNICATION) + } + + @Test + fun `a transient focus loss does not pause a voice message`() { + assertThat(losesFocusOn(AudioFocusRequester.VoiceMessage, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)).isFalse() + assertThat(losesFocusOn(AudioFocusRequester.VoiceMessage, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK)).isFalse() + } + + @Test + fun `a permanent focus loss pauses a voice message`() { + assertThat(losesFocusOn(AudioFocusRequester.VoiceMessage, AudioManager.AUDIOFOCUS_LOSS)).isTrue() + } + + @Test + fun `a transient focus loss does not stop a voice message recording`() { + assertThat(losesFocusOn(AudioFocusRequester.RecordVoiceMessage, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)).isFalse() + } + + @Test + fun `a transient focus loss still pauses the media viewer`() { + assertThat(losesFocusOn(AudioFocusRequester.MediaViewer, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)).isTrue() + } + + private fun losesFocusOn(requester: AudioFocusRequester, focusChange: Int): Boolean { + val context = RuntimeEnvironment.getApplication() + val audioManager = requireNotNull(context.getSystemService()) + var focusLost = false + DefaultAudioFocus(context).requestAudioFocus(requester) { focusLost = true } + val request = requireNotNull(shadowOf(audioManager).lastAudioFocusRequest) + request.listener.onAudioFocusChange(focusChange) + return focusLost + } + + private fun usageOf(requester: AudioFocusRequester): Int { + val context = RuntimeEnvironment.getApplication() + val audioManager = requireNotNull(context.getSystemService()) + DefaultAudioFocus(context).requestAudioFocus(requester) {} + val request = requireNotNull(shadowOf(audioManager).lastAudioFocusRequest) + return requireNotNull(request.audioFocusRequest).audioAttributes.usage + } +} diff --git a/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/di/CacheStoreModule.kt b/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/di/CacheStoreBindingContainer.kt similarity index 96% rename from libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/di/CacheStoreModule.kt rename to libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/di/CacheStoreBindingContainer.kt index 89de82a08f..c28cc0601c 100644 --- a/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/di/CacheStoreModule.kt +++ b/libraries/cachestore/impl/src/main/kotlin/io/element/android/libraries/cachestore/impl/di/CacheStoreBindingContainer.kt @@ -22,10 +22,10 @@ import timber.log.Timber @BindingContainer @ContributesTo(AppScope::class) -object CacheStoreModule { +object CacheStoreBindingContainer { @Provides @SingleIn(AppScope::class) - fun provideCacheDatabase( + fun providesCacheDatabase( @ApplicationContext context: Context, ): CacheDatabase { val name = "cache_database" diff --git a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt index bf5932da59..e519984e9f 100644 --- a/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt +++ b/libraries/compound/src/main/kotlin/io/element/android/compound/theme/Theme.kt @@ -8,8 +8,11 @@ package io.element.android.compound.theme -import androidx.compose.foundation.isSystemInDarkTheme +import android.content.res.Configuration import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -27,12 +30,28 @@ private fun Theme.coerceBlackTheme(allowBlackTheme: Boolean): Theme { @Composable fun Theme.isDark(): Boolean { return when (this) { - Theme.System -> isSystemInDarkTheme() + Theme.System -> isSystemThemeDark() Theme.Dark, Theme.Black -> true Theme.Light -> false } } +/** + * Whether the system-wide theme is dark. + * + * This deliberately does not use [androidx.compose.foundation.isSystemInDarkTheme], which reads the activity configuration: `AppCompatDelegate` rewrites + * that configuration whenever a non-system theme is selected, so it keeps reporting the previously forced value. The application configuration is left + * untouched by AppCompat and is what AppCompat itself reads to expand `MODE_NIGHT_FOLLOW_SYSTEM`. + */ +@Composable +private fun isSystemThemeDark(): Boolean { + val applicationContext = LocalContext.current.applicationContext + val activityConfiguration = LocalConfiguration.current + return remember(activityConfiguration) { + applicationContext.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + } +} + fun Flow.mapToTheme(allowBlackTheme: Boolean): Flow = map { when (it) { null -> Theme.System diff --git a/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt b/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt index 3f43cfea67..57ae9dd332 100644 --- a/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt +++ b/libraries/compound/src/test/kotlin/io/element/android/compound/theme/ThemeTest.kt @@ -11,19 +11,22 @@ package io.element.android.compound.theme import android.content.res.Configuration import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext 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.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Test +import org.robolectric.RuntimeEnvironment -class ThemeTest { +class ThemeTest : RobolectricTest() { @Test fun `isDark for System dark returns true`() { `isDark for System`( - uiMode = Configuration.UI_MODE_NIGHT_YES, + systemUiMode = Configuration.UI_MODE_NIGHT_YES, expected = true, ) } @@ -31,21 +34,38 @@ class ThemeTest { @Test fun `isDark for System light return false`() { `isDark for System`( - uiMode = Configuration.UI_MODE_NIGHT_NO, + systemUiMode = Configuration.UI_MODE_NIGHT_NO, expected = false, ) } - fun `isDark for System`( - uiMode: Int, + @Test + fun `isDark for System ignores the night mode AppCompat forced on the activity`() { + `isDark for System`( + systemUiMode = Configuration.UI_MODE_NIGHT_YES, + activityUiMode = Configuration.UI_MODE_NIGHT_NO, + expected = true, + ) + `isDark for System`( + systemUiMode = Configuration.UI_MODE_NIGHT_NO, + activityUiMode = Configuration.UI_MODE_NIGHT_YES, + expected = false, + ) + } + + private fun `isDark for System`( + systemUiMode: Int, + activityUiMode: Int = systemUiMode, expected: Boolean, ) = runTest { + val application = RuntimeEnvironment.getApplication() + application.resources.configuration.uiMode = systemUiMode moleculeFlow(RecompositionMode.Immediate) { var result: Boolean? = null CompositionLocalProvider( - // Let set the system to dark + LocalContext provides application, LocalConfiguration provides Configuration().apply { - this.uiMode = uiMode + this.uiMode = activityUiMode }, ) { result = Theme.System.isDark() diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt index fe563138ad..87d6350c65 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.core.extensions +import java.text.Normalizer import java.util.Locale fun Boolean.toOnOff() = if (this) "ON" else "OFF" @@ -114,3 +115,14 @@ fun String.toSafeLength( this } } + +/** + * Remove accents and replace special characters by ASCII from the string. + * For instance, "é" will be replaced by "e", "ç" by "c", etc. + */ +fun String.normalized(): String { + // Normalize the string. + return Normalizer.normalize(this, Normalizer.Form.NFD) + // then keep only ASCII characters and replace non-ASCII characters with an empty string. + .replace("[^\\p{ASCII}]".toRegex(), "") +} diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt index e7962f8bd6..3901a32059 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt @@ -63,4 +63,17 @@ object MimeTypes { val subType = components.last() return subType.isNotBlank() && subType != "*" } + + /** + * A mime type with a wildcard subtype is a filter, not a media type, so replace it with a + * concrete default for its top level type. + */ + fun String?.ensureDefaultSubtype(): String = when { + this == null -> OctetStream + hasSubtype(this) -> this + this == Images -> Jpeg + this == Videos -> Mp4 + this == Audio -> Mp3 + else -> OctetStream + } } diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/uri/UrlUtils.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/uri/UrlUtils.kt index 666ee1d7ab..91402e3d11 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/uri/UrlUtils.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/uri/UrlUtils.kt @@ -20,12 +20,13 @@ fun String.isValidUrl(): Boolean { } /** - * Ensure string starts with "http". If it is not the case, "https://" is added, only if the String is not empty + * Ensure string starts with "http" (case-insensitively, so "HTTP://" / "HTTPS://" are respected). If it is + * not the case, "https://" is added, only if the String is not empty. */ fun String.ensureProtocol(): String { return when { isEmpty() -> this - !startsWith("http") -> "https://$this" + !startsWith("http", ignoreCase = true) -> "https://$this" else -> this } } diff --git a/libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/BasicExtensionsTest.kt b/libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/BasicExtensionsTest.kt index 0099a67a79..c3cae14843 100644 --- a/libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/BasicExtensionsTest.kt +++ b/libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/BasicExtensionsTest.kt @@ -74,4 +74,10 @@ class BasicExtensionsTest { val result = textWithDirectionOverrides.filterDirectionOverrides() assertEquals("123456789", result) } + + @Test + fun `normalized removes accents and replace special characters from text`() { + assertEquals("Stefan", "Ștefan".normalized()) + assertEquals("ca enleve etre", "ça enlève être".normalized()) + } } diff --git a/libraries/core/src/test/kotlin/io/element/android/libraries/core/mimetype/MimeTypesTest.kt b/libraries/core/src/test/kotlin/io/element/android/libraries/core/mimetype/MimeTypesTest.kt new file mode 100644 index 0000000000..f12b73b40b --- /dev/null +++ b/libraries/core/src/test/kotlin/io/element/android/libraries/core/mimetype/MimeTypesTest.kt @@ -0,0 +1,35 @@ +/* + * 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.libraries.core.mimetype + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.core.mimetype.MimeTypes.ensureDefaultSubtype +import org.junit.Test + +class MimeTypesTest { + @Test + fun `ensureDefaultSubtype keeps a mime type that already has a subtype`() { + assertThat(MimeTypes.Png.ensureDefaultSubtype()).isEqualTo(MimeTypes.Png) + assertThat(MimeTypes.Mp4.ensureDefaultSubtype()).isEqualTo(MimeTypes.Mp4) + assertThat(MimeTypes.Pdf.ensureDefaultSubtype()).isEqualTo(MimeTypes.Pdf) + } + + @Test + fun `ensureDefaultSubtype replaces a wildcard subtype with a concrete one`() { + assertThat(MimeTypes.Images.ensureDefaultSubtype()).isEqualTo(MimeTypes.Jpeg) + assertThat(MimeTypes.Videos.ensureDefaultSubtype()).isEqualTo(MimeTypes.Mp4) + assertThat(MimeTypes.Audio.ensureDefaultSubtype()).isEqualTo(MimeTypes.Mp3) + } + + @Test + fun `ensureDefaultSubtype falls back to octet stream`() { + assertThat(MimeTypes.Any.ensureDefaultSubtype()).isEqualTo(MimeTypes.OctetStream) + assertThat("text/*".ensureDefaultSubtype()).isEqualTo(MimeTypes.OctetStream) + assertThat(null.ensureDefaultSubtype()).isEqualTo(MimeTypes.OctetStream) + } +} diff --git a/libraries/core/src/test/kotlin/io/element/android/libraries/core/uri/UrlUtilsTest.kt b/libraries/core/src/test/kotlin/io/element/android/libraries/core/uri/UrlUtilsTest.kt new file mode 100644 index 0000000000..9a9d68dcaa --- /dev/null +++ b/libraries/core/src/test/kotlin/io/element/android/libraries/core/uri/UrlUtilsTest.kt @@ -0,0 +1,35 @@ +/* + * 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.libraries.core.uri + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class UrlUtilsTest { + @Test + fun `ensureProtocol keeps an empty string unchanged`() { + assertThat("".ensureProtocol()).isEqualTo("") + } + + @Test + fun `ensureProtocol prepends https when there is no scheme`() { + assertThat("matrix.org".ensureProtocol()).isEqualTo("https://matrix.org") + } + + @Test + fun `ensureProtocol keeps an existing http or https scheme`() { + assertThat("https://matrix.org".ensureProtocol()).isEqualTo("https://matrix.org") + assertThat("http://matrix.org".ensureProtocol()).isEqualTo("http://matrix.org") + } + + @Test + fun `ensureProtocol keeps an uppercase scheme rather than double-prepending`() { + assertThat("HTTPS://matrix.org".ensureProtocol()).isEqualTo("HTTPS://matrix.org") + assertThat("Http://matrix.org".ensureProtocol()).isEqualTo("Http://matrix.org") + } +} diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyRepository.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyRepository.kt index bcd38695c4..fe27c49f36 100644 --- a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyRepository.kt +++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/KeyStoreSecretKeyRepository.kt @@ -26,6 +26,8 @@ import java.util.concurrent.ConcurrentHashMap import javax.crypto.KeyGenerator import javax.crypto.SecretKey +internal const val ANDROID_KEYSTORE = "AndroidKeyStore" + /** * Default implementation of [SecretKeyRepository] that uses the Android Keystore to store the keys. * The generated key uses AES algorithm, with a key size of 128 bits, and the GCM block mode. diff --git a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/CryptographyModule.kt b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/di/CryptographyBindingContainer.kt similarity index 79% rename from libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/CryptographyModule.kt rename to libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/di/CryptographyBindingContainer.kt index e9a9eb7b34..bdfe6f9e5d 100644 --- a/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/CryptographyModule.kt +++ b/libraries/cryptography/impl/src/main/kotlin/io/element/android/libraries/cryptography/impl/di/CryptographyBindingContainer.kt @@ -6,19 +6,18 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.cryptography.impl +package io.element.android.libraries.cryptography.impl.di import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.BindingContainer import dev.zacsweers.metro.ContributesTo import dev.zacsweers.metro.Provides +import io.element.android.libraries.cryptography.impl.ANDROID_KEYSTORE import java.security.KeyStore -internal const val ANDROID_KEYSTORE = "AndroidKeyStore" - @ContributesTo(AppScope::class) @BindingContainer -object CryptographyModule { +object CryptographyBindingContainer { @Provides fun providesAndroidKeyStore(): KeyStore { return KeyStore.getInstance(ANDROID_KEYSTORE).apply { diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterBindingContainer.kt similarity index 84% rename from libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt rename to libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterBindingContainer.kt index c80324c149..ee8afe678f 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterBindingContainer.kt @@ -19,7 +19,7 @@ import kotlin.time.Clock @BindingContainer @ContributesTo(AppScope::class) -object DateFormatterModule { +object DateFormatterBindingContainer { @Provides fun providesClock(): Clock = Clock.System @@ -27,5 +27,5 @@ object DateFormatterModule { fun providesLocale(): Locale = Locale.getDefault() @Provides - fun providesTimezone(): TimezoneProvider = TimezoneProvider { TimeZone.currentSystemDefault() } + fun providesTimezoneProvider(): TimezoneProvider = TimezoneProvider { TimeZone.currentSystemDefault() } } diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeProvider.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModePreviewParam.kt similarity index 86% rename from libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeProvider.kt rename to libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModePreviewParam.kt index 48856cad7b..f00911294e 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeProvider.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModePreviewParam.kt @@ -11,7 +11,7 @@ package io.element.android.libraries.dateformatter.impl.previews import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.dateformatter.api.DateFormatterMode -class DateFormatterModeProvider : PreviewParameterProvider { +class DateFormatterModePreviewParam : PreviewParameterProvider { override val values: Sequence get() = DateFormatterMode.entries.asSequence() } diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeViewPreview.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeViewPreview.kt index e3cd9563a8..5e1d35a568 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeViewPreview.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeViewPreview.kt @@ -34,7 +34,7 @@ import kotlin.time.Instant @Preview @Composable internal fun DateFormatterModeViewPreview( - @PreviewParameter(DateFormatterModeProvider::class) dateFormatterMode: DateFormatterMode, + @PreviewParameter(DateFormatterModePreviewParam::class) dateFormatterMode: DateFormatterMode, ) = ElementPreview { DateFormatterModeView(dateFormatterMode) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt index 8b0f17177b..08d2f05fbc 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMolecule.kt @@ -134,7 +134,7 @@ enum class ComposerAlertLevel { @PreviewsDayNight @Composable internal fun ComposerAlertMoleculePreview( - @PreviewParameter(ComposerAlertMoleculeParamsProvider::class) params: ComposerAlertMoleculeParams, + @PreviewParameter(ComposerAlertMoleculeParamsPreviewParam::class) params: ComposerAlertMoleculeParams, ) = ElementPreview { ComposerAlertMolecule( avatar = params.avatar, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMoleculeParamsProvider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMoleculeParamsPreviewParam.kt similarity index 91% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMoleculeParamsProvider.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMoleculeParamsPreviewParam.kt index cd1bc8a02b..487cd06dfc 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMoleculeParamsProvider.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/ComposerAlertMoleculeParamsPreviewParam.kt @@ -19,7 +19,7 @@ internal data class ComposerAlertMoleculeParams( val showIcon: Boolean = false, ) -internal class ComposerAlertMoleculeParamsProvider : PreviewParameterProvider { +internal class ComposerAlertMoleculeParamsPreviewParam : PreviewParameterProvider { private val allLevels = sequenceOf( ComposerAlertLevel.Info, ComposerAlertLevel.Critical diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt index 639d365625..cf6329ee66 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/BigIcon.kt @@ -160,7 +160,7 @@ internal fun BigIconPreview() = ElementPreview { horizontalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(10.dp), ) { - items(BigIconStyleProvider().values.toList()) { style -> + items(BigIconStylePreviewParam().values.toList()) { style -> Box( contentAlignment = Alignment.Center ) { @@ -170,7 +170,7 @@ internal fun BigIconPreview() = ElementPreview { } } -internal class BigIconStyleProvider : PreviewParameterProvider { +internal class BigIconStylePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( BigIcon.Style.Default(Icons.Filled.CatchingPokemon), diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncActionProvider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncActionPreviewParam.kt similarity index 90% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncActionProvider.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncActionPreviewParam.kt index 2f7439e1c8..9fe699b828 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncActionProvider.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncActionPreviewParam.kt @@ -11,7 +11,7 @@ package io.element.android.libraries.designsystem.components.async import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncAction -open class AsyncActionProvider : PreviewParameterProvider> { +open class AsyncActionPreviewParam : PreviewParameterProvider> { override val values: Sequence> get() = sequenceOf( AsyncAction.Uninitialized, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncActionView.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncActionView.kt index da1b0fc796..65dc628eb3 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncActionView.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncActionView.kt @@ -82,7 +82,7 @@ object AsyncActionViewDefaults { @PreviewsDayNight @Composable internal fun AsyncActionViewPreview( - @PreviewParameter(AsyncActionProvider::class) async: AsyncAction, + @PreviewParameter(AsyncActionPreviewParam::class) async: AsyncAction, ) = ElementPreview { AsyncActionView( async = async, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt index ac7e426e65..99c45c67d9 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt @@ -8,9 +8,11 @@ package io.element.android.libraries.designsystem.components.avatar +import androidx.compose.runtime.Immutable import io.element.android.libraries.core.data.tryOrNull import java.text.BreakIterator +@Immutable data class AvatarData( val id: String, val name: String?, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarRow.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarRow.kt index 25784aa501..b80105350d 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarRow.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarRow.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.components.avatar.internal.OverlapRatioProvider +import io.element.android.libraries.designsystem.components.avatar.internal.OverlapRatioPreviewParam import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toPx @@ -110,7 +110,7 @@ fun AvatarRow( @Composable @PreviewsDayNight -internal fun AvatarRowPreview(@PreviewParameter(OverlapRatioProvider::class) overlapRatio: Float) { +internal fun AvatarRowPreview(@PreviewParameter(OverlapRatioPreviewParam::class) overlapRatio: Float) { ElementPreview { ContentToPreview(overlapRatio) } @@ -118,7 +118,7 @@ internal fun AvatarRowPreview(@PreviewParameter(OverlapRatioProvider::class) ove @Composable @PreviewsDayNight -internal fun AvatarRowLastOnTopPreview(@PreviewParameter(OverlapRatioProvider::class) overlapRatio: Float) { +internal fun AvatarRowLastOnTopPreview(@PreviewParameter(OverlapRatioPreviewParam::class) overlapRatio: Float) { ElementPreview { ContentToPreview( overlapRatio = overlapRatio, @@ -129,7 +129,7 @@ internal fun AvatarRowLastOnTopPreview(@PreviewParameter(OverlapRatioProvider::c @Composable @PreviewsDayNight -internal fun AvatarRowRtlPreview(@PreviewParameter(OverlapRatioProvider::class) overlapRatio: Float) { +internal fun AvatarRowRtlPreview(@PreviewParameter(OverlapRatioPreviewParam::class) overlapRatio: Float) { CompositionLocalProvider( LocalLayoutDirection provides LayoutDirection.Rtl, ) { @@ -141,7 +141,7 @@ internal fun AvatarRowRtlPreview(@PreviewParameter(OverlapRatioProvider::class) @Composable @PreviewsDayNight -internal fun AvatarRowLastOnTopRtlPreview(@PreviewParameter(OverlapRatioProvider::class) overlapRatio: Float) { +internal fun AvatarRowLastOnTopRtlPreview(@PreviewParameter(OverlapRatioPreviewParam::class) overlapRatio: Float) { CompositionLocalProvider( LocalLayoutDirection provides LayoutDirection.Rtl, ) { diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index eb27780073..b91f8c405c 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -8,9 +8,11 @@ package io.element.android.libraries.designsystem.components.avatar +import androidx.compose.runtime.Immutable import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +@Immutable enum class AvatarSize(val dp: Dp) { CurrentUserTopBar(32.dp), CurrentRoomTopBar(32.dp), @@ -18,6 +20,7 @@ enum class AvatarSize(val dp: Dp) { IncomingCall(140.dp), RoomDetailsHeader(96.dp), RoomListItem(52.dp), + SearchRoomListItem(32.dp), ThreadsListItem(52.dp), SpaceListItem(52.dp), diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/OverlapRatioProvider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/OverlapRatioPreviewParam.kt similarity index 88% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/OverlapRatioProvider.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/OverlapRatioPreviewParam.kt index 26a76c2fac..90f9c7c1c2 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/OverlapRatioProvider.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/internal/OverlapRatioPreviewParam.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.designsystem.components.avatar.internal import androidx.compose.ui.tooling.preview.PreviewParameterProvider -class OverlapRatioProvider : PreviewParameterProvider { +class OverlapRatioPreviewParam : PreviewParameterProvider { override val values: Sequence = sequenceOf( 0f, 0.25f, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/ImageVectorProvider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/ImageVectorPreviewParam.kt similarity index 89% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/ImageVectorProvider.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/ImageVectorPreviewParam.kt index c1bf2b9112..530452cd60 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/ImageVectorProvider.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/ImageVectorPreviewParam.kt @@ -13,7 +13,7 @@ import androidx.compose.material.icons.filled.BugReport import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.PreviewParameterProvider -open class ImageVectorProvider : PreviewParameterProvider { +open class ImageVectorPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( Icons.Default.BugReport, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/PreferenceIcon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/PreferenceIcon.kt index 59e818c18a..5a2e3518f2 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/PreferenceIcon.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/components/PreferenceIcon.kt @@ -86,7 +86,7 @@ private fun PreferenceIcon( @Preview(group = PreviewGroup.Preferences) @Composable -internal fun PreferenceIconPreview(@PreviewParameter(ImageVectorProvider::class) content: ImageVector?) = +internal fun PreferenceIconPreview(@PreviewParameter(ImageVectorPreviewParam::class) content: ImageVector?) = ElementThemedPreview { PreferenceIcon( icon = content, @@ -96,7 +96,7 @@ internal fun PreferenceIconPreview(@PreviewParameter(ImageVectorProvider::class) @Preview(group = PreviewGroup.Preferences) @Composable -internal fun PreferenceIconWithBadgePreview(@PreviewParameter(ImageVectorProvider::class) content: ImageVector?) = +internal fun PreferenceIconWithBadgePreview(@PreviewParameter(ImageVectorPreviewParam::class) content: ImageVector?) = ElementThemedPreview { PreferenceIcon( icon = content, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/BooleanProvider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/BooleanPreviewParam.kt similarity index 86% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/BooleanProvider.kt rename to libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/BooleanPreviewParam.kt index c50e20dd66..05715464b5 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/BooleanProvider.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/BooleanPreviewParam.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.designsystem.utils import androidx.compose.ui.tooling.preview.PreviewParameterProvider -open class BooleanProvider : PreviewParameterProvider { +open class BooleanPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf(true, false) } diff --git a/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerStateProvider.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerStatePreviewParam.kt similarity index 96% rename from libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerStateProvider.kt rename to libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerStatePreviewParam.kt index 964e0f3b78..e649670da4 100644 --- a/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerStateProvider.kt +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/DefaultEmojiPickerStatePreviewParam.kt @@ -19,7 +19,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -internal class DefaultEmojiPickerStateProvider : PreviewParameterProvider { +internal class DefaultEmojiPickerStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aDefaultEmojiPickerState(), diff --git a/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiPickerView.kt b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiPickerView.kt index 88faa3a19a..c41cbb3b57 100644 --- a/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiPickerView.kt +++ b/libraries/emoji/impl/src/main/kotlin/io/element/android/libraries/emoji/impl/picker/EmojiPickerView.kt @@ -166,7 +166,7 @@ private fun EmojiResults( @PreviewsDayNight @Composable -internal fun EmojiPickerViewPreview(@PreviewParameter(DefaultEmojiPickerStateProvider::class) state: DefaultEmojiPickerState) = ElementPreview { +internal fun EmojiPickerViewPreview(@PreviewParameter(DefaultEmojiPickerStatePreviewParam::class) state: DefaultEmojiPickerState) = ElementPreview { EmojiPickerView( state = state, onSelectEmoji = {}, diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 79b63f4455..4a8894e99c 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -152,13 +152,6 @@ enum class FeatureFlags( defaultValue = { false }, isFinished = false, ), - UserStatus( - key = "feature.user_status", - title = "User status", - description = "Allow users to set a status (e.g. In a meeting, Away) visible to their contacts.", - defaultValue = { false }, - isFinished = false, - ), MessageSearch( key = "feature.message_search", title = "Message search", diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagModule.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagBindingContainer.kt similarity index 96% rename from libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagModule.kt rename to libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagBindingContainer.kt index de3f8aa519..be4fa6f23f 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagModule.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/di/FeatureFlagBindingContainer.kt @@ -18,7 +18,7 @@ import io.element.android.libraries.featureflag.impl.PreferencesFeatureFlagProvi @BindingContainer @ContributesTo(AppScope::class) -object FeatureFlagModule { +object FeatureFlagBindingContainer { @JvmStatic @Provides @ElementsIntoSet diff --git a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsEvents.kt b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsEvent.kt similarity index 62% rename from libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsEvents.kt rename to libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsEvent.kt index 1c5bc4b6a0..fc39eb96a9 100644 --- a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsEvents.kt +++ b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsEvent.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.fullscreenintent.api -sealed interface FullScreenIntentPermissionsEvents { - data object Dismiss : FullScreenIntentPermissionsEvents - data object OpenSettings : FullScreenIntentPermissionsEvents +sealed interface FullScreenIntentPermissionsEvent { + data object Dismiss : FullScreenIntentPermissionsEvent + data object OpenSettings : FullScreenIntentPermissionsEvent } diff --git a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsState.kt b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsState.kt index 5b1ddb91ed..238e11bd6c 100644 --- a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsState.kt +++ b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsState.kt @@ -11,5 +11,5 @@ package io.element.android.libraries.fullscreenintent.api data class FullScreenIntentPermissionsState( val permissionGranted: Boolean, val shouldDisplayBanner: Boolean, - val eventSink: (FullScreenIntentPermissionsEvents) -> Unit, + val eventSink: (FullScreenIntentPermissionsEvent) -> Unit, ) diff --git a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsStateProvider.kt b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsStateProvider.kt index 7248a24414..2e42908851 100644 --- a/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsStateProvider.kt +++ b/libraries/fullscreenintent/api/src/main/kotlin/io/element/android/libraries/fullscreenintent/api/FullScreenIntentPermissionsStateProvider.kt @@ -11,7 +11,7 @@ package io.element.android.libraries.fullscreenintent.api fun aFullScreenIntentPermissionsState( permissionGranted: Boolean = true, shouldDisplay: Boolean = false, - eventSink: (FullScreenIntentPermissionsEvents) -> Unit = {}, + eventSink: (FullScreenIntentPermissionsEvent) -> Unit = {}, ) = FullScreenIntentPermissionsState( permissionGranted = permissionGranted, shouldDisplayBanner = shouldDisplay, diff --git a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt index d4a3e75d1a..7d726ccdcc 100644 --- a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt +++ b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/FullScreenIntentPermissionsPresenter.kt @@ -25,7 +25,7 @@ import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta -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.preferences.api.store.PreferenceDataStoreFactory import io.element.android.services.toolbox.api.intent.ExternalIntentLauncher @@ -64,12 +64,12 @@ class FullScreenIntentPermissionsPresenter( val isGranted = notificationManagerCompat.canUseFullScreenIntent() val isBannerDismissed by isFullScreenIntentBannerDismissed.collectAsState(initial = true) - fun handleEvent(event: FullScreenIntentPermissionsEvents) { + fun handleEvent(event: FullScreenIntentPermissionsEvent) { when (event) { - FullScreenIntentPermissionsEvents.Dismiss -> coroutineScope.launch { + FullScreenIntentPermissionsEvent.Dismiss -> coroutineScope.launch { dismissFullScreenIntentBanner() } - FullScreenIntentPermissionsEvents.OpenSettings -> openFullScreenIntentSettings() + FullScreenIntentPermissionsEvent.OpenSettings -> openFullScreenIntentSettings() } } diff --git a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/di/FullScreenIntentModule.kt b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/di/FullScreenIntentBindingContainer.kt similarity index 95% rename from libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/di/FullScreenIntentModule.kt rename to libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/di/FullScreenIntentBindingContainer.kt index 249485b15c..ae43cebd46 100644 --- a/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/di/FullScreenIntentModule.kt +++ b/libraries/fullscreenintent/impl/src/main/kotlin/io/element/android/libraries/fullscreenintent/impl/di/FullScreenIntentBindingContainer.kt @@ -18,7 +18,7 @@ import io.element.android.libraries.fullscreenintent.impl.FullScreenIntentPermis @ContributesTo(AppScope::class) @BindingContainer -interface FullScreenIntentModule { +interface FullScreenIntentBindingContainer { @Binds fun bindFullScreenIntentPermissionsPresenter(presenter: FullScreenIntentPermissionsPresenter): Presenter } diff --git a/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt b/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt index 44a5579e86..08bc2ed885 100644 --- a/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt +++ b/libraries/fullscreenintent/impl/src/test/kotlin/io/element/android/libraries/fullscreenintent/test/FullScreenIntentPermissionsPresenterTest.kt @@ -16,7 +16,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvents +import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvent import io.element.android.libraries.fullscreenintent.impl.FullScreenIntentPermissionsPresenter import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory @@ -78,7 +78,7 @@ class FullScreenIntentPermissionsPresenterTest { }.test { skipItems(1) val loadedItem = awaitItem() - loadedItem.eventSink(FullScreenIntentPermissionsEvents.Dismiss) + loadedItem.eventSink(FullScreenIntentPermissionsEvent.Dismiss) runCurrent() assertThat(awaitItem().shouldDisplayBanner).isFalse() } @@ -94,7 +94,7 @@ class FullScreenIntentPermissionsPresenterTest { }.test { skipItems(1) val loadedItem = awaitItem() - loadedItem.eventSink(FullScreenIntentPermissionsEvents.OpenSettings) + loadedItem.eventSink(FullScreenIntentPermissionsEvent.OpenSettings) launchLambda.assertions().isCalledOnce() cancelAndIgnoreRemainingEvents() } @@ -113,7 +113,7 @@ class FullScreenIntentPermissionsPresenterTest { }.test { skipItems(1) val loadedItem = awaitItem() - loadedItem.eventSink(FullScreenIntentPermissionsEvents.OpenSettings) + loadedItem.eventSink(FullScreenIntentPermissionsEvent.OpenSettings) launchLambda.assertions().isNeverCalled() cancelAndIgnoreRemainingEvents() } diff --git a/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt b/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt index 0629799c74..1dd2a4c64c 100644 --- a/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt +++ b/libraries/indicator/impl/src/main/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorService.kt @@ -9,11 +9,14 @@ package io.element.android.libraries.indicator.impl import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.di.SessionScope import io.element.android.libraries.indicator.api.IndicatorService @@ -43,12 +46,17 @@ class DefaultIndicatorService( override fun showSettingChatBackupIndicator(): State { val backupState by encryptionService.backupStateStateFlow.collectAsState() val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState() + var doesBackupExistOnServer by remember { mutableStateOf(null) } + + LaunchedEffect(backupState) { + if (backupState == BackupState.UNKNOWN) { + doesBackupExistOnServer = encryptionService.doesBackupExistOnServer().getOrNull() + } + } return remember { derivedStateOf { - val showForBackup = backupState in listOf( - BackupState.UNKNOWN, - ) + val showForBackup = backupState == BackupState.UNKNOWN && doesBackupExistOnServer != true val showForRecovery = recoveryState in listOf( RecoveryState.DISABLED, RecoveryState.INCOMPLETE, diff --git a/libraries/indicator/impl/src/test/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorServiceTest.kt b/libraries/indicator/impl/src/test/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorServiceTest.kt index cd861ef530..3782e2c9f1 100644 --- a/libraries/indicator/impl/src/test/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorServiceTest.kt +++ b/libraries/indicator/impl/src/test/kotlin/io/element/android/libraries/indicator/impl/DefaultIndicatorServiceTest.kt @@ -42,8 +42,10 @@ class DefaultIndicatorServiceTest { } @Test - fun `test - showSettingChatBackupIndicator is true when BackupState is UNKNOWN`() = runTest { - val encryptionService = FakeEncryptionService() + fun `test - showSettingChatBackupIndicator is true when BackupState is UNKNOWN and no backup exists on the server`() = runTest { + val encryptionService = FakeEncryptionService().apply { + givenDoesBackupExistOnServerResult(Result.success(false)) + } val sessionVerificationService = FakeSessionVerificationService() val sut = DefaultIndicatorService( sessionVerificationService = sessionVerificationService, @@ -61,6 +63,24 @@ class DefaultIndicatorServiceTest { } } + @Test + fun `test - showSettingChatBackupIndicator is false when BackupState is UNKNOWN but a backup exists on the server`() = runTest { + val encryptionService = FakeEncryptionService().apply { + givenDoesBackupExistOnServerResult(Result.success(true)) + } + val sessionVerificationService = FakeSessionVerificationService() + val sut = DefaultIndicatorService( + sessionVerificationService = sessionVerificationService, + encryptionService = encryptionService, + ) + moleculeFlow(RecompositionMode.Immediate) { + sut.showSettingChatBackupIndicator().value + }.test { + assertThat(awaitItem()).isTrue() + assertThat(awaitItem()).isFalse() + } + } + @Test fun `test - showSettingChatBackupIndicator is true when recoveryState is DISABLED`() = runTest { val encryptionService = FakeEncryptionService() diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/UrlContentFetcher.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/ClientUrlContentFetcher.kt similarity index 63% rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/UrlContentFetcher.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/ClientUrlContentFetcher.kt index a6fb234b86..611bf7bf18 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/UrlContentFetcher.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/ClientUrlContentFetcher.kt @@ -8,9 +8,19 @@ package io.element.android.libraries.matrix.api /** - * An interface to fetch content from a URL using a simple GET request. + * An interface to fetch content from a URL using a simple GET request launched from a Matrix client. */ -fun interface UrlContentFetcher { +interface ClientUrlContentFetcher { + /** + * The domain of the public server for this homeserver. + */ + val server: String? + + /** + * The URL of the actual Matrix homeserver. + */ + val homeserverUrl: String + /** * Execute generic GET requests through the SDKs internal HTTP client. */ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index b83d96af96..3f98dcc509 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -60,16 +60,13 @@ import java.util.Optional * One instance exists per session, created on login or session restore and torn down by [logout], [clearCache] or account deactivation. * Suspending members here report failures through a [Result] instead of throwing, and run their work off the main thread. */ -interface MatrixClient : UrlContentFetcher { +interface MatrixClient : ClientUrlContentFetcher { /** Identifier of the logged in user; a [SessionId] and a [UserId] are the same value. */ val sessionId: SessionId /** Identifier of this device, as assigned by the homeserver when the session was created. */ val deviceId: DeviceId - /** Base URL of the homeserver this client is connected to. */ - val homeserverUrl: String - /** Directories holding this session's databases, caches and downloaded media. */ val sessionPaths: SessionPaths @@ -136,6 +133,9 @@ interface MatrixClient : UrlContentFetcher { /** Scans media content before it is displayed, or `null` when no content scanner is configured for this session. */ val contentScanner: ContentScanner? + /** Whether the session is in the process of shutting down, either because it is being logged out or its cache is being cleared. */ + val isShuttingDown: Boolean + /** * Returns the room with the given id if the user has joined it, along with its live [Timeline]. * A new instance is created on each call and the caller owns it, so it must be closed once no longer needed. @@ -245,6 +245,9 @@ interface MatrixClient : UrlContentFetcher { /** Whether the homeserver advertises support for user status (MSC4426). */ suspend fun isUserStatusSupported(): Result + /** Whether the homeserver advertises support for the Profiles sliding sync extension (MSC4262). */ + suspend fun isProfilesSlidingSyncExtensionSupported(): Result + /** * Enable or disable automatically setting the user's status to "in a call" (m.call) while in a call. * diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientBuilder.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientBuilder.kt new file mode 100644 index 0000000000..730561bbef --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientBuilder.kt @@ -0,0 +1,13 @@ +/* + * 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.libraries.matrix.api + +/** + * An abstraction over the Matrix SDK's `ClientBuilder` to allow sharing it with other modules without exposing the SDK directly. + */ +interface MatrixClientBuilder diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/TemporaryMatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/TemporaryMatrixClient.kt index 1e3ffea154..5013362f85 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/TemporaryMatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/TemporaryMatrixClient.kt @@ -15,4 +15,4 @@ import java.lang.AutoCloseable * Note: this client implements an [AutoCloseable] interface, so using [AutoCloseable.close] or [AutoCloseable.use] will clean up any resources associated * with it, such as temporary files. */ -interface TemporaryMatrixClient : UrlContentFetcher, AutoCloseable +interface TemporaryMatrixClient : ClientUrlContentFetcher, AutoCloseable diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/TemporaryMatrixClientFactory.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/TemporaryMatrixClientFactory.kt index 3bd22c64f3..c3244b4742 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/TemporaryMatrixClientFactory.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/TemporaryMatrixClientFactory.kt @@ -12,10 +12,10 @@ package io.element.android.libraries.matrix.api */ interface TemporaryMatrixClientFactory { /** - * Builds an unauthenticated client pointing at [homeServerUrl], backed by a fresh set of temporary session directories. + * Builds an unauthenticated client pointing at [serverName], backed by a fresh set of temporary session directories. * The caller owns the returned client and must close it to delete those directories. * - * @param homeServerUrl the URL of the homeserver the client will talk to. + * @param serverName the domain of the public-facing server for the actual homeserver */ - suspend fun create(homeServerUrl: String): Result + suspend fun create(serverName: String): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/scanner/ContentScannerUrlProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/scanner/ContentScannerUrlProvider.kt deleted file mode 100644 index 2c11c750e4..0000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/scanner/ContentScannerUrlProvider.kt +++ /dev/null @@ -1,31 +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.libraries.matrix.api.scanner - -import io.element.android.libraries.matrix.api.UrlContentFetcher -import io.element.android.libraries.matrix.api.core.SessionId - -/** - * Provides the URL of the content scanner service for a given homeserver, if any is set up. - */ -fun interface ContentScannerUrlProvider { - /** - * Returns the URL of the content scanner service for the given [sessionId], or `null` if no content scanner is set up. - */ - suspend fun getContentScannerUrl(sessionId: SessionId): Result - - /** - * Creates a provider, which needs a fetcher because the scanner URL is itself discovered over HTTP. - */ - fun interface Factory { - /** - * @param urlContentFetcher used to read the configuration advertised by the homeserver; a client that is not authenticated yet also works. - */ - fun create(urlContentFetcher: UrlContentFetcher): ContentScannerUrlProvider - } -} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt index 2f61b8f58a..f2bd56ac57 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt @@ -128,4 +128,11 @@ sealed interface GalleryItemType { data class OtherMessageType( val msgType: String, val body: String, -) : MessageType +) : MessageType { + /** + * A `m.key.verification.request` carries a body meant for clients that cannot handle in-chat + * verification, so it must not be rendered as a message. + */ + val isKeyVerificationRequest: Boolean + get() = msgType == "m.key.verification.request" +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 0ff563a204..a36184d6bd 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -135,6 +135,7 @@ import org.matrix.rustcomponents.sdk.use import timber.log.Timber import java.io.File import java.util.Optional +import java.util.concurrent.atomic.AtomicBoolean import kotlin.jvm.optionals.getOrNull import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -145,7 +146,7 @@ import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService @Suppress("LargeClass") class RustMatrixClient( override val sessionPaths: SessionPaths, - private val innerClient: Client, + val innerClient: Client, private val sessionStore: SessionStore, private val sessionDelegate: RustClientSessionDelegate, private val innerSyncService: ClientSyncService, @@ -162,6 +163,7 @@ class RustMatrixClient( ) : MatrixClient { override val sessionId: UserId = UserId(innerClient.userId()) override val deviceId: DeviceId = DeviceId(innerClient.deviceId()) + override val server: String? = innerClient.server() override val homeserverUrl: String = innerClient.homeserver() override val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-$sessionId") private val sessionDispatcher = dispatchers.io.limitedParallelism(64) @@ -304,6 +306,10 @@ class RustMatrixClient( .buffer(Channel.UNLIMITED) .stateIn(sessionCoroutineScope, started = SharingStarted.Eagerly, initialValue = persistentListOf()) + private val _isShuttingDown = AtomicBoolean(false) + override val isShuttingDown: Boolean + get() = _isShuttingDown.get() + init { // Make sure the session delegate has a reference to the client to be able to logout on auth error sessionDelegate.bindClient(this) @@ -320,7 +326,9 @@ class RustMatrixClient( } private suspend fun setupUserProfile() { - val supported = isUserStatusSupported().getOrDefault(false) + // Subscribing to own profile updates only requires the Profiles sliding sync extension, + // not the full user status capability (which also needs the status profile field to be settable). + val supported = isProfilesSlidingSyncExtensionSupported().getOrDefault(false) if (supported) { // No need to seed the data here, it's already stored by the sdk. ownProfileTaskHandle = innerClient.subscribeToOwnProfile(ownProfileListener) @@ -530,6 +538,12 @@ class RustMatrixClient( } } + override suspend fun isProfilesSlidingSyncExtensionSupported(): Result = withContext(sessionDispatcher) { + runCatchingExceptions { + innerClient.isProfilesSlidingSyncExtensionSupported() + } + } + override fun enableAutomaticCallStatus(enabled: Boolean) { innerClient.enableAutomaticCallStatus(enabled) } @@ -666,11 +680,13 @@ class RustMatrixClient( } override suspend fun clearCache() { + _isShuttingDown.set(true) innerClient.clearCaches(innerSyncService) destroy() } override suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean) { + _isShuttingDown.set(true) sessionCoroutineScope.cancel() // Remove current delegate so we don't receive an auth error clientDelegateTaskHandle?.cancelAndDestroy() @@ -708,6 +724,8 @@ class RustMatrixClient( } override suspend fun deactivateAccount(password: String, eraseData: Boolean): Result = withContext(sessionDispatcher) { + _isShuttingDown.set(true) + Timber.w("Deactivating account") // Remove current delegate so we don't receive an auth error clientDelegateTaskHandle?.cancelAndDestroy() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientBuilder.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientBuilder.kt new file mode 100644 index 0000000000..b2f952f35b --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientBuilder.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.libraries.matrix.impl + +import io.element.android.libraries.matrix.api.MatrixClientBuilder +import org.matrix.rustcomponents.sdk.ClientBuilder + +/** + * A wrapper around the Matrix SDK's [ClientBuilder] to allow sharing it with other modules without exposing the SDK directly. + */ +class RustMatrixClientBuilder( + val inner: ClientBuilder, +) : MatrixClientBuilder diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt index a0b06a2e1e..0d593626bb 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl import dev.zacsweers.metro.Inject +import io.element.android.features.enterprise.api.ClientBuilderEnterpriseHook import io.element.android.libraries.androidutils.crypto.ClientSecret import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.data.ByteUnit @@ -19,7 +20,6 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.paths.SessionPaths -import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider import io.element.android.libraries.matrix.impl.analytics.UtdTracker import io.element.android.libraries.matrix.impl.paths.getSessionPaths import io.element.android.libraries.matrix.impl.proxy.ProxyProvider @@ -37,7 +37,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientBuilder -import org.matrix.rustcomponents.sdk.ContentScanner import org.matrix.rustcomponents.sdk.CrossProcessLockConfig import org.matrix.rustcomponents.sdk.RequestConfig import org.matrix.rustcomponents.sdk.Session @@ -70,7 +69,7 @@ class RustMatrixClientFactory( private val clientBuilderProvider: ClientBuilderProvider, private val sqliteStoreBuilderProvider: SqliteStoreBuilderProvider, private val workManagerScheduler: WorkManagerScheduler, - private val contentScannerUrlProviderFactory: ContentScannerUrlProvider.Factory, + private val clientBuilderEnterpriseHook: ClientBuilderEnterpriseHook, ) { private val sessionDelegate = RustClientSessionDelegate( sessionStore = sessionStore, @@ -101,6 +100,7 @@ class RustMatrixClientFactory( ) .homeserverUrl(sessionData.homeserverUrl) .username(sessionData.userId) + .let { (clientBuilderEnterpriseHook(RustMatrixClientBuilder(it), SessionId(sessionData.userId)) as RustMatrixClientBuilder).inner } .use { it.build() } client.setMediaRetentionPolicy( @@ -135,21 +135,6 @@ class RustMatrixClientFactory( client.setUtdDelegate(UtdTracker(analyticsService)) - // If a content scanner URL is available for the homeserver, create a RustContentScanner and set it on the client. - // This allows the SDK to use the content scanner for automatic media scanning. - // If no content scanner URL is available, the contentScanner will be null. - val contentScannerUrlProvider = contentScannerUrlProviderFactory.create(RustTemporaryMatrixClient(client, null)) - val contentScanner = contentScannerUrlProvider.getContentScannerUrl(SessionId(client.userId())) - .getOrNull() - ?.let { contentScannerUrl -> - val contentScanner = ContentScanner(contentScannerUrl) - client.setContentScanner(contentScanner) - RustContentScanner( - client = client, - rustScanner = contentScanner, - ) - } - val syncService = client.syncService() .withSharePos(true) .withOfflineMode() @@ -170,7 +155,7 @@ class RustMatrixClientFactory( featureFlagService = featureFlagService, analyticsService = analyticsService, workManagerScheduler = workManagerScheduler, - contentScanner = contentScanner, + contentScanner = client.contentScanner()?.let { RustContentScanner(client, it) }, isMessageSearchAvailable = isMessageSearchAvailable, ).also { Timber.tag("RustMatrixClient").i("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'") diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClient.kt index 54e4f696d3..f5b1f2ba4e 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClient.kt @@ -22,6 +22,9 @@ class RustTemporaryMatrixClient( client.getUrl(url) }.mapFailure { it.mapClientException() } + override val server: String? = client.server() + override val homeserverUrl: String = client.homeserver() + override fun close() { client.close() paths?.let { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientFactory.kt index 70903ba91a..0e7d8bf2c5 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientFactory.kt @@ -19,7 +19,7 @@ class RustTemporaryMatrixClientFactory( private val sessionPathsFactory: SessionPathsFactory, private val rustMatrixClientFactory: RustMatrixClientFactory, ) : TemporaryMatrixClientFactory { - override suspend fun create(homeServerUrl: String): Result { + override suspend fun create(serverName: String): Result { return runCatchingExceptions { val sessionPaths = sessionPathsFactory.create() val client = rustMatrixClientFactory.getBaseClientBuilder( @@ -28,7 +28,7 @@ class RustTemporaryMatrixClientFactory( slidingSyncType = ClientBuilderSlidingSync.Native, isMessageSearchAvailable = false, ) - .homeserverUrl(homeServerUrl) + .serverName(serverName) .build() RustTemporaryMatrixClient(client, sessionPaths) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt index fac5227f6a..6a06ac0599 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt @@ -29,6 +29,7 @@ fun Throwable.mapAuthenticationException(): AuthenticationException { is ClientBuildException.WellKnownLookupFailed -> AuthenticationException.Generic(message) is ClientBuildException.EventCache -> AuthenticationException.Generic(message) is ClientBuildException.InvalidRawKey -> AuthenticationException.Generic(message) + is ClientBuildException.WellKnownLookupDisabled -> AuthenticationException.Generic(message) } is OAuthException -> when (this) { is OAuthException.Generic -> AuthenticationException.OAuth(message) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index 2c936c9b23..a1dacbee91 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.impl.auth import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn +import io.element.android.features.enterprise.api.ClientEnterpriseHook import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.libraries.androidutils.crypto.ClientSecret import io.element.android.libraries.core.coroutine.CoroutineDispatchers @@ -73,6 +74,7 @@ class RustMatrixAuthenticationService( private val oAuthConfigurationProvider: OAuthConfigurationProvider, private val enterpriseService: EnterpriseService, private val featureFlagService: FeatureFlagService, + private val clientEnterpriseHook: ClientEnterpriseHook, ) : MatrixAuthenticationService { // Any existing Element Classic session that we want to try to import secrets from during login. private var elementClassicSession: ElementClassicSession? = null @@ -166,6 +168,10 @@ class RustMatrixAuthenticationService( sessionPaths = currentSessionPaths, ) val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable()) + + // Apply enterprise hooks to the newly created client as soon as possible + clientEnterpriseHook(matrixClient) + newMatrixClientObservers.forEach { it.invoke(matrixClient) } sessionStore.addSession(sessionData) @@ -242,6 +248,9 @@ class RustMatrixAuthenticationService( client.restoreSession(sessionData.toSession()) val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable()) + // Apply enterprise hooks to the newly created client as soon as possible + clientEnterpriseHook(matrixClient) + // We wait for the verification state to be known matrixClient.waitForKnownVerificationState() @@ -278,7 +287,6 @@ class RustMatrixAuthenticationService( .let { enterpriseService.tweakMasUrl( url = it, - homeserver = client.server() ?: client.homeserver(), urlContentFetcher = getUrlResolver, ) } @@ -333,6 +341,10 @@ class RustMatrixAuthenticationService( sessionPaths = currentSessionPaths, ) val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable()) + + // Apply enterprise hooks to the newly created client as soon as possible + clientEnterpriseHook(matrixClient) + matrixClient.waitForKnownVerificationState() newMatrixClientObservers.forEach { it.invoke(matrixClient) } @@ -398,6 +410,10 @@ class RustMatrixAuthenticationService( sessionPaths = emptySessionPaths, ) val matrixClient = rustMatrixClientFactory.create(client, sessionData, isMessageSearchAvailable()) + + // Apply enterprise hooks to the newly created client as soon as possible + clientEnterpriseHook(matrixClient) + newMatrixClientObservers.forEach { it.invoke(matrixClient) } sessionStore.addSession(sessionData) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/RoomModule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/RoomBindingContainer.kt similarity index 96% rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/RoomModule.kt rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/RoomBindingContainer.kt index af6f0325d3..d598f85bad 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/RoomModule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/RoomBindingContainer.kt @@ -18,7 +18,7 @@ import kotlinx.coroutines.CoroutineScope @BindingContainer @ContributesTo(RoomScope::class) -object RoomModule { +object RoomBindingContainer { @RoomCoroutineScope @Provides fun providesSessionCoroutineScope(room: BaseRoom): CoroutineScope { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixBindingContainer.kt similarity index 88% rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixBindingContainer.kt index 46fa885d25..f50c0a04cc 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixBindingContainer.kt @@ -13,9 +13,9 @@ import dev.zacsweers.metro.ContributesTo import dev.zacsweers.metro.Provides import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.matrix.api.ClientUrlContentFetcher import io.element.android.libraries.matrix.api.HomeserverCapabilitiesProvider import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.UrlContentFetcher import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.encryption.EncryptionService @@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.scanner.ContentScanner +import io.element.android.libraries.matrix.api.search.MessageSearchService import io.element.android.libraries.matrix.api.spaces.SpaceService import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.verification.SessionVerificationService @@ -33,7 +34,7 @@ import kotlinx.coroutines.CoroutineScope @BindingContainer @ContributesTo(SessionScope::class) -object SessionMatrixModule { +object SessionMatrixBindingContainer { @Provides fun providesSessionId(matrixClient: MatrixClient): SessionId { return matrixClient.sessionId @@ -55,7 +56,7 @@ object SessionMatrixModule { } @Provides - fun provideRoomMembershipObserver(matrixClient: MatrixClient): RoomMembershipObserver { + fun providesRoomMembershipObserver(matrixClient: MatrixClient): RoomMembershipObserver { return matrixClient.roomMembershipObserver } @@ -111,7 +112,12 @@ object SessionMatrixModule { } @Provides - fun providesGetUrlResolverClient(matrixClient: MatrixClient): UrlContentFetcher { + fun providesClientUrlContentFetcher(matrixClient: MatrixClient): ClientUrlContentFetcher { return matrixClient } + + @Provides + fun providesMessageSearchService(matrixClient: MatrixClient): MessageSearchService { + return matrixClient.messageSearchService + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt index d1401d2624..7b3a1d9f10 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt @@ -11,7 +11,7 @@ package io.element.android.libraries.matrix.impl.media import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.core.extensions.runCatchingExceptions -import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.core.mimetype.MimeTypes.ensureDefaultSubtype import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaSource @@ -71,15 +71,8 @@ class RustMediaLoader( val mediaFile = innerClient.getMediaFile( mediaSource = mediaSource, filename = filename, - mimeType = when { - mimeType == null -> MimeTypes.OctetStream - MimeTypes.hasSubtype(mimeType) -> mimeType - // Fallback to a default mime type based on the main type, so that the SDK can create a file with the correct extension. - mimeType == MimeTypes.Images -> MimeTypes.Jpeg - mimeType == MimeTypes.Videos -> MimeTypes.Mp4 - mimeType == MimeTypes.Audio -> MimeTypes.Mp3 - else -> MimeTypes.OctetStream - }, + // Fallback to a default mime type based on the main type, so that the SDK can create a file with the correct extension. + mimeType = mimeType.ensureDefaultSubtype(), useCache = useCache, tempDir = cacheDirectory.path, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt index e59530e874..a5f372d7d1 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt @@ -8,11 +8,13 @@ package io.element.android.libraries.matrix.impl.permalink +import android.net.Uri import androidx.core.net.toUri import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.MatrixPatterns 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 @@ -51,6 +53,10 @@ class DefaultPermalinkParser( matrixToConverter.convert(uri) ?: return PermalinkData.FallbackLink(uri) } + matrixToUri.userIdWithSlash()?.let { userId -> + return PermalinkData.UserLink(userId = userId) + } + val result = runCatchingExceptions { parseMatrixEntityFrom(matrixToUri.toString()) }.getOrNull() @@ -84,3 +90,17 @@ class DefaultPermalinkParser( } } } + +/** + * A slash is allowed in the local part of a user id, but it also separates an identifier from an + * event id in a permalink, so [parseMatrixEntityFrom] cannot tell the two apart. Only a user id can + * start with an `@`, so recover it here rather than letting the ambiguity reach the SDK. + */ +private fun Uri.userIdWithSlash(): UserId? { + val identifier = fragment + ?.removePrefix("/") + ?.substringBefore('?') + ?: return null + if (!identifier.startsWith("@") || !identifier.contains('/')) return null + return identifier.takeIf { MatrixPatterns.isUserId(it) }?.let(::UserId) +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index bd756bc002..c6a737e156 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -75,6 +75,7 @@ import org.matrix.rustcomponents.sdk.PollData import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle import org.matrix.rustcomponents.sdk.UploadParameters import org.matrix.rustcomponents.sdk.UploadSource +import org.matrix.rustcomponents.sdk.createCaptionEdit import org.matrix.rustcomponents.sdk.use import timber.log.Timber import uniffi.matrix_sdk.PaginationStatus @@ -341,7 +342,7 @@ class RustTimeline( formattedCaption: String?, ): Result = withContext(dispatcher) { runCatchingExceptions { - val editedContent = EditedContent.MediaCaption( + val editedContent = createCaptionEdit( caption = caption, formattedCaption = formattedCaption?.let { FormattedBody(body = it, format = MessageFormat.Html) @@ -365,7 +366,7 @@ class RustTimeline( fromNotification: Boolean, msgType: MsgType, ): Result = withContext(dispatcher) { - runCatchingExceptions { + runCatchingExceptions { val msg = MessageEventContent.from( body = body, htmlBody = htmlBody, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt index 5e5000aedc..6d3afad5c3 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt @@ -11,7 +11,6 @@ package io.element.android.libraries.matrix.impl import com.google.common.truth.Truth.assertThat import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider @@ -52,7 +51,6 @@ fun TestScope.createRustMatrixClientFactory( ), clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(), workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(), - contentScannerUrlProviderFactory: ContentScannerUrlProvider.Factory = { ContentScannerUrlProvider { Result.success(null) } }, ) = RustMatrixClientFactory( cacheDirectory = cacheDirectory, appCoroutineScope = backgroundScope, @@ -67,5 +65,5 @@ fun TestScope.createRustMatrixClientFactory( clientBuilderProvider = clientBuilderProvider, sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(), workManagerScheduler = workManagerScheduler, - contentScannerUrlProviderFactory = contentScannerUrlProviderFactory, + clientBuilderEnterpriseHook = { builder, _ -> builder }, ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientFactoryTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientFactoryTest.kt index a8843a9ca2..d0b87ef53a 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientFactoryTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientFactoryTest.kt @@ -9,7 +9,6 @@ package io.element.android.libraries.matrix.impl import com.google.common.truth.Truth.assertThat import io.element.android.libraries.featureflag.test.FakeFeatureFlagService -import io.element.android.libraries.matrix.api.scanner.ContentScannerUrlProvider import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory @@ -64,7 +63,6 @@ class RustTemporaryMatrixClientFactoryTest { ), clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(), workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(), - contentScannerUrlProviderFactory: ContentScannerUrlProvider.Factory = { ContentScannerUrlProvider { Result.success(null) } }, ) = RustMatrixClientFactory( cacheDirectory = cacheDirectory, appCoroutineScope = backgroundScope, @@ -79,6 +77,6 @@ class RustTemporaryMatrixClientFactoryTest { clientBuilderProvider = clientBuilderProvider, sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(), workManagerScheduler = workManagerScheduler, - contentScannerUrlProviderFactory = contentScannerUrlProviderFactory, + clientBuilderEnterpriseHook = { builder, _ -> builder }, ) } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientTest.kt index 322103042b..23e0cf2302 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustTemporaryMatrixClientTest.kt @@ -16,7 +16,6 @@ import io.element.android.tests.testutils.lambda.value import kotlinx.coroutines.test.runTest import org.junit.Test import org.matrix.rustcomponents.sdk.Client -import org.matrix.rustcomponents.sdk.NoHandle import java.io.File import kotlin.io.path.createTempDirectory import org.matrix.rustcomponents.sdk.ClientException as RustClientException @@ -84,7 +83,7 @@ class RustTemporaryMatrixClientTest { } private fun createRustTemporaryMatrixClient( - client: Client = Client(NoHandle), + client: Client = FakeFfiClient(), paths: SessionPaths, ): RustTemporaryMatrixClient { return RustTemporaryMatrixClient(client, paths) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt index e349c616c0..5d66e4f4bd 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationServiceTest.kt @@ -98,6 +98,7 @@ class RustMatrixAuthenticationServiceTest { ), enterpriseService = enterpriseService, featureFlagService = FakeFeatureFlagService(), + clientEnterpriseHook = {}, ) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt index 13be3cf2d5..86587da6c1 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt @@ -10,13 +10,16 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession import io.element.android.libraries.matrix.impl.fixtures.factories.aRustUserProfile +import io.element.android.libraries.matrix.impl.scanner.FakeFfiContentScanner import io.element.android.libraries.matrix.test.A_DEVICE_ID import io.element.android.libraries.matrix.test.A_HOMESERVER_URL +import io.element.android.libraries.matrix.test.A_SERVER_NAME import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.simulateLongTask import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientDelegate +import org.matrix.rustcomponents.sdk.ContentScanner import org.matrix.rustcomponents.sdk.CreateRoomParameters import org.matrix.rustcomponents.sdk.Encryption import org.matrix.rustcomponents.sdk.HomeserverCapabilities @@ -45,6 +48,7 @@ class FakeFfiClient( private val userId: String = A_USER_ID.value, private val deviceId: String = A_DEVICE_ID.value, private val homeserver: String = A_HOMESERVER_URL, + private val server: String? = A_SERVER_NAME, private val notificationClient: NotificationClient = FakeFfiNotificationClient(), private val notificationSettings: NotificationSettings = FakeFfiNotificationSettings(), private val encryption: Encryption = FakeFfiEncryption(), @@ -59,13 +63,16 @@ class FakeFfiClient( private val accountDataResult: (String) -> String? = { lambdaError() }, private val setAccountDataResult: (String, String) -> Unit = { _, _ -> lambdaError() }, private val isUserStatusSupportedResult: () -> Boolean = { false }, + private val isProfilesSlidingSyncExtensionSupportedResult: () -> Boolean = { false }, private val subscribeToOwnProfileResult: (ProfileListener) -> Unit = {}, private val getUrlResult: (String) -> ByteArray = { lambdaError() }, + private val contentScannerResult: () -> ContentScanner = { FakeFfiContentScanner() }, private val closeResult: () -> Unit = {}, ) : Client(NoHandle) { override fun userId(): String = userId override fun deviceId(): String = deviceId override fun homeserver(): String = homeserver + override fun server(): String? = server override suspend fun notificationClient(processSetup: NotificationProcessSetup) = notificationClient override suspend fun getNotificationSettings(): NotificationSettings = notificationSettings override fun encryption(): Encryption = encryption @@ -100,6 +107,8 @@ class FakeFfiClient( override suspend fun isUserStatusSupported(): Boolean = isUserStatusSupportedResult() + override suspend fun isProfilesSlidingSyncExtensionSupported(): Boolean = isProfilesSlidingSyncExtensionSupportedResult() + override fun subscribeToOwnProfile(listener: ProfileListener): TaskHandle { subscribeToOwnProfileResult(listener) return FakeFfiTaskHandle() @@ -139,5 +148,9 @@ class FakeFfiClient( getUrlResult(url) } + override suspend fun contentScanner(): ContentScanner = simulateLongTask { + contentScannerResult() + } + override fun close() = closeResult() } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParserTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParserTest.kt new file mode 100644 index 0000000000..416213a0b2 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParserTest.kt @@ -0,0 +1,44 @@ +/* + * 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.libraries.matrix.impl.permalink + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.permalink.PermalinkData +import io.element.android.tests.testutils.robolectric.RobolectricTest +import org.junit.Test + +class DefaultPermalinkParserTest : RobolectricTest() { + @Test + fun `a matrix-to link to a user id with a slash is parsed as a user link`() { + val result = createParser().parse("https://matrix.to/#/@it/sme:matrix.org") + assertThat(result).isEqualTo(PermalinkData.UserLink(UserId("@it/sme:matrix.org"))) + } + + @Test + fun `an escaped slash in a user id is parsed as a user link`() { + val result = createParser().parse("https://matrix.to/#/@it%2Fsme:matrix.org") + assertThat(result).isEqualTo(PermalinkData.UserLink(UserId("@it/sme:matrix.org"))) + } + + @Test + fun `an element web link to a user id with a slash is parsed as a user link`() { + val result = createParser().parse("https://app.element.io/#/user/@it/sme:matrix.org") + assertThat(result).isEqualTo(PermalinkData.UserLink(UserId("@it/sme:matrix.org"))) + } + + @Test + fun `a user id with a slash that is not a valid user id is not a user link`() { + val result = createParser().parse("https://example.org/foo/@it/sme") + assertThat(result).isInstanceOf(PermalinkData.FallbackLink::class.java) + } + + private fun createParser() = DefaultPermalinkParser( + matrixToConverter = DefaultMatrixToConverter(), + ) +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeClientUrlContentFetcher.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeClientUrlContentFetcher.kt new file mode 100644 index 0000000000..2018228985 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeClientUrlContentFetcher.kt @@ -0,0 +1,21 @@ +/* + * 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.libraries.matrix.test + +import io.element.android.libraries.matrix.api.ClientUrlContentFetcher +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeClientUrlContentFetcher( + override val server: String = A_SERVER_NAME, + override val homeserverUrl: String = A_HOMESERVER_URL, + private val getUrlResult: (String) -> Result = { lambdaError() }, +) : ClientUrlContentFetcher { + override suspend fun getUrl(url: String): Result { + return getUrlResult(url) + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 56d25ed175..6ba42270fe 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -79,6 +79,7 @@ class FakeMatrixClient( override val sessionId: SessionId = A_SESSION_ID, override val sessionPaths: SessionPaths = SessionPaths(fileDirectory = File("files"), cacheDirectory = File("cache")), override val deviceId: DeviceId = A_DEVICE_ID, + override val server: String = A_SERVER_NAME, override val homeserverUrl: String = A_HOMESERVER_URL, override val sessionCoroutineScope: CoroutineScope = TestScope(), private val userDisplayName: String? = A_USER_NAME, @@ -99,7 +100,7 @@ class FakeMatrixClient( override val roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(), private val homeserverCapabilitiesProvider: FakeHomeserverCapabilitiesProvider = FakeHomeserverCapabilitiesProvider(), private val accountManagementUrlResult: (AccountManagementAction?) -> Result = { lambdaError() }, - private val resolveRoomAliasResult: (RoomAlias) -> Result> = { + private val resolveRoomAliasResult: suspend (RoomAlias) -> Result> = { Result.success( Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList())) ) @@ -133,6 +134,7 @@ class FakeMatrixClient( private val resetWellKnownConfigLambda: () -> Result = { lambdaError() }, private val enableAutomaticCallStatusLambda: (Boolean) -> Unit = { }, override val contentScanner: ContentScanner? = null, + private val isShuttingDownResult: () -> Boolean = { false }, ) : MatrixClient { var setDisplayNameCalled: Boolean = false private set @@ -147,6 +149,7 @@ class FakeMatrixClient( var setUserStatusResult: Result = Result.success(Unit) var clearUserStatusResult: Result = Result.success(Unit) var isUserStatusSupportedResult: Result = Result.success(true) + var isProfilesSlidingSyncExtensionSupportedResult: Result = Result.success(true) private val _userProfile: MutableStateFlow = MutableStateFlow(MatrixUser(sessionId, userDisplayName, userAvatarUrl)) override val userProfile: StateFlow = _userProfile @@ -293,6 +296,8 @@ class FakeMatrixClient( override suspend fun isUserStatusSupported(): Result = isUserStatusSupportedResult + override suspend fun isProfilesSlidingSyncExtensionSupported(): Result = isProfilesSlidingSyncExtensionSupportedResult + override fun enableAutomaticCallStatus(enabled: Boolean) = enableAutomaticCallStatusLambda(enabled) override suspend fun joinRoom(roomId: RoomId): Result = joinRoomLambda(roomId) @@ -305,6 +310,9 @@ class FakeMatrixClient( return knockRoomLambda(roomIdOrAlias, message, serverNames) } + override val isShuttingDown: Boolean + get() = isShuttingDownResult() + // Mocks fun givenCreateRoomResult(result: Result) { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeTemporaryMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeTemporaryMatrixClient.kt index 83bde4653a..3ee7fb5f2f 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeTemporaryMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeTemporaryMatrixClient.kt @@ -11,10 +11,11 @@ import io.element.android.libraries.matrix.api.TemporaryMatrixClient import io.element.android.tests.testutils.lambda.lambdaError class FakeTemporaryMatrixClient( + override val server: String = A_SERVER_NAME, + override val homeserverUrl: String = A_HOMESERVER_URL, private val getUrlResult: (String) -> Result = { lambdaError() }, private val closeLambda: () -> Unit = {}, ) : TemporaryMatrixClient { override suspend fun getUrl(url: String): Result = getUrlResult(url) - override fun close() = closeLambda() } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeTemporaryMatrixClientFactory.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeTemporaryMatrixClientFactory.kt index cdec68bdd1..e906777dcb 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeTemporaryMatrixClientFactory.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeTemporaryMatrixClientFactory.kt @@ -13,5 +13,5 @@ import io.element.android.libraries.matrix.api.TemporaryMatrixClientFactory class FakeTemporaryMatrixClientFactory( private val createResult: (String) -> Result = { Result.success(FakeTemporaryMatrixClient()) }, ) : TemporaryMatrixClientFactory { - override suspend fun create(homeServerUrl: String): Result = createResult(homeServerUrl) + override suspend fun create(serverName: String): Result = createResult(serverName) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index c41ce0fe3e..c8f49eaf56 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -72,6 +72,7 @@ const val A_SPACE_NAME = "A space name" const val A_REDACTION_REASON = "A redaction reason" +const val A_SERVER_NAME = "matrix.org" const val A_HOMESERVER_URL = "matrix.org" const val A_HOMESERVER_URL_2 = "matrix-client.org" diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt index 3b2fdeafb1..1eac6c0a1d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeMatrixAuthenticationService.kt @@ -70,10 +70,15 @@ class FakeMatrixAuthenticationService( return importCreatedSessionLambda(externalSession) } + /** The login hint passed to the most recent [getOAuthUrl] call. */ + var getOAuthUrlLoginHint: String? = null + private set + override suspend fun getOAuthUrl( prompt: OAuthPrompt, loginHint: String?, ): Result = simulateLongTask { + getOAuthUrlLoginHint = loginHint oAuthError?.let { Result.failure(it) } ?: Result.success(AN_OAUTH_DATA) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt index ebc8790a8a..38c5732875 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt @@ -132,7 +132,7 @@ data class AttachmentThumbnailInfo( @PreviewsDayNight @Composable -internal fun AttachmentThumbnailPreview(@PreviewParameter(AttachmentThumbnailInfoProvider::class) data: AttachmentThumbnailInfo) = ElementPreview { +internal fun AttachmentThumbnailPreview(@PreviewParameter(AttachmentThumbnailInfoPreviewParam::class) data: AttachmentThumbnailInfo) = ElementPreview { AttachmentThumbnail( info = data, modifier = Modifier diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnailInfoProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnailInfoPreviewParam.kt similarity index 94% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnailInfoProvider.kt rename to libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnailInfoPreviewParam.kt index ea38a979ae..1509ac2144 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnailInfoProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnailInfoPreviewParam.kt @@ -11,7 +11,7 @@ package io.element.android.libraries.matrix.ui.components import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.matrix.api.media.MediaSource -open class AttachmentThumbnailInfoProvider : PreviewParameterProvider { +open class AttachmentThumbnailInfoPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( anAttachmentThumbnailInfo(type = AttachmentThumbnailType.Image), diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt index 598d7f2ca7..45835ed479 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/CreateDmConfirmationBottomSheet.kt @@ -158,7 +158,7 @@ fun CreateDmConfirmationBottomSheet( @Composable internal fun CreateDmConfirmationBottomSheetPreview( @PreviewParameter( - CreateDmConfirmationBottomSheetStateProvider::class + CreateDmConfirmationBottomSheetStatePreviewParam::class ) state: CreateDmConfirmationBottomSheetState ) = ElementPreview(fillMaxSize = true) { CreateDmConfirmationBottomSheet( @@ -174,7 +174,7 @@ data class CreateDmConfirmationBottomSheetState( val isUserIdentityUnknown: Boolean, ) -class CreateDmConfirmationBottomSheetStateProvider : PreviewParameterProvider { +class CreateDmConfirmationBottomSheetStatePreviewParam : PreviewParameterProvider { override val values = sequenceOf( CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), isUserIdentityUnknown = false), CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), isUserIdentityUnknown = true), diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt index 89b0532580..188a8bc1a4 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserHeader.kt @@ -79,6 +79,6 @@ fun MatrixUserHeader( @PreviewsDayNight @Composable -internal fun MatrixUserHeaderPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = ElementPreview { +internal fun MatrixUserHeaderPreview(@PreviewParameter(MatrixUserPreviewParam::class) matrixUser: MatrixUser) = ElementPreview { MatrixUserHeader(matrixUser) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserPreviewParam.kt similarity index 94% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt rename to libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserPreviewParam.kt index 7bc7d00f09..034570310e 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserPreviewParam.kt @@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.DisplayedStatus import io.element.android.libraries.matrix.api.user.MatrixUser -open class MatrixUserProvider : PreviewParameterProvider { +open class MatrixUserPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMatrixUser(), @@ -33,7 +33,7 @@ open class MatrixUserProvider : PreviewParameterProvider { ) } -open class MatrixUserWithAvatarProvider : PreviewParameterProvider { +open class MatrixUserWithAvatarPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMatrixUser(displayName = USER_NAME_JOHN_DOE), diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt index 9fc7ea7b4a..2a202486a3 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserRow.kt @@ -46,6 +46,6 @@ fun MatrixUserRow( @PreviewsDayNight @Composable -internal fun MatrixUserRowPreview(@PreviewParameter(MatrixUserProvider::class) matrixUser: MatrixUser) = ElementPreview { +internal fun MatrixUserRowPreview(@PreviewParameter(MatrixUserPreviewParam::class) matrixUser: MatrixUser) = ElementPreview { MatrixUserRow(matrixUser) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectRoomInfoProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectRoomInfoPreviewParam.kt similarity index 95% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectRoomInfoProvider.kt rename to libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectRoomInfoPreviewParam.kt index c6a4b9d474..e205bac1dd 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectRoomInfoProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectRoomInfoPreviewParam.kt @@ -16,7 +16,7 @@ import io.element.android.libraries.matrix.ui.model.SelectRoomInfo import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -class SelectRoomInfoProvider : PreviewParameterProvider { +class SelectRoomInfoPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aSelectRoomInfo(roomId = RoomId("!room1:domain")), diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt index b7484903da..9ebaa5b22c 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt @@ -51,7 +51,7 @@ fun SelectedRoom( @PreviewsDayNight @Composable internal fun SelectedRoomPreview( - @PreviewParameter(SelectRoomInfoProvider::class) roomInfo: SelectRoomInfo + @PreviewParameter(SelectRoomInfoPreviewParam::class) roomInfo: SelectRoomInfo ) = ElementPreview { SelectedRoom( roomInfo = roomInfo, @@ -62,7 +62,7 @@ internal fun SelectedRoomPreview( @PreviewsDayNight @Composable internal fun SelectedRoomRtlPreview( - @PreviewParameter(SelectRoomInfoProvider::class) roomInfo: SelectRoomInfo + @PreviewParameter(SelectRoomInfoPreviewParam::class) roomInfo: SelectRoomInfo ) = CompositionLocalProvider( LocalLayoutDirection provides LayoutDirection.Rtl, ) { diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt index 17ec7c6ec7..ce8c047cef 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedUser.kt @@ -44,7 +44,7 @@ fun SelectedUser( @PreviewsDayNight @Composable -internal fun SelectedUserPreview(@PreviewParameter(MatrixUserWithAvatarProvider::class) user: MatrixUser) = ElementPreview { +internal fun SelectedUserPreview(@PreviewParameter(MatrixUserWithAvatarPreviewParam::class) user: MatrixUser) = ElementPreview { SelectedUser( matrixUser = user, canRemove = true, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt index d3824f9eec..d10698662f 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomItemView.kt @@ -210,7 +210,7 @@ private fun SpaceRoomItemScaffold( @Composable @PreviewsDayNight -internal fun SpaceRoomItemViewPreview(@PreviewParameter(SpaceRoomProvider::class) spaceRoom: SpaceRoom) = ElementPreview { +internal fun SpaceRoomItemViewPreview(@PreviewParameter(SpaceRoomPreviewParam::class) spaceRoom: SpaceRoom) = ElementPreview { SpaceRoomItemView( spaceRoom = spaceRoom, showUnreadIndicator = spaceRoom.state == CurrentUserMembership.INVITED, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomPreviewParam.kt similarity index 97% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt rename to libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomPreviewParam.kt index d6a582d83c..0eddafd9da 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SpaceRoomPreviewParam.kt @@ -17,7 +17,7 @@ import io.element.android.libraries.matrix.api.room.RoomType 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( roomType = RoomType.Room, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsPreviewParam.kt similarity index 93% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt rename to libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsPreviewParam.kt index b335ef9609..1b48e0eb9b 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsPreviewParam.kt @@ -32,10 +32,11 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent 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.user.DisplayedStatus import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf -open class InReplyToDetailsProvider : PreviewParameterProvider { +open class InReplyToDetailsPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMessageContent( @@ -99,7 +100,7 @@ open class InReplyToDetailsProvider : PreviewParameterProvider } } -class InReplyToDetailsDisambiguatedProvider : InReplyToDetailsProvider() { +class InReplyToDetailsDisambiguatedPreviewParam : InReplyToDetailsPreviewParam() { override val values: Sequence get() = sequenceOf( aMessageContent( @@ -114,7 +115,7 @@ class InReplyToDetailsDisambiguatedProvider : InReplyToDetailsProvider() { } } -class InReplyToDetailsInformativeProvider : InReplyToDetailsProvider() { +class InReplyToDetailsInformativePreviewParam : InReplyToDetailsPreviewParam() { override val values: Sequence get() = sequenceOf( RedactedContent, @@ -126,7 +127,7 @@ class InReplyToDetailsInformativeProvider : InReplyToDetailsProvider() { } } -class InReplyToDetailsOtherProvider : InReplyToDetailsProvider() { +class InReplyToDetailsOtherPreviewParam : InReplyToDetailsPreviewParam() { override val values: Sequence get() = sequenceOf( InReplyToDetails.Loading(eventId = EventId("\$anEventId")), @@ -163,9 +164,10 @@ fun aProfileDetailsReady( displayName: String? = USER_NAME_SENDER, displayNameAmbiguous: Boolean = false, avatarUrl: String? = null, + displayedStatus: DisplayedStatus? = null ) = ProfileDetails.Ready( displayName = displayName, displayNameAmbiguous = displayNameAmbiguous, avatarUrl = avatarUrl, - displayedStatus = null, + displayedStatus = displayedStatus, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToView.kt index 45a9ef2427..8cdc3ae24c 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToView.kt @@ -236,7 +236,7 @@ private fun ReplyToNotFoundContent( @PreviewsDayNight @Composable -internal fun InReplyToViewPreview(@PreviewParameter(provider = InReplyToDetailsProvider::class) inReplyTo: InReplyToDetails) = ElementPreview { +internal fun InReplyToViewPreview(@PreviewParameter(provider = InReplyToDetailsPreviewParam::class) inReplyTo: InReplyToDetails) = ElementPreview { InReplyToView( inReplyTo = inReplyTo, hideImage = false, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderName.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderName.kt index 42432c6950..7cd066d0ce 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderName.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderName.kt @@ -9,12 +9,15 @@ package io.element.android.libraries.matrix.ui.messages.sender import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -24,7 +27,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails -import io.element.android.libraries.matrix.ui.model.toText +import io.element.android.libraries.matrix.ui.model.toEmojiText // https://www.figma.com/file/Ni6Ii8YKtmXCKYNE90cC67/Timeline-(new)?type=design&node-id=917-80169&mode=design&t=A0CJCBbMqR8NOwUQ-0 @Composable @@ -61,7 +64,7 @@ fun SenderName( modifier = Modifier .clipToBounds() .alignByBaseline(), - text = userStatus.toText(), + text = userStatus.toEmojiText(), style = ElementTheme.typography.fontBodyMdMedium, color = ElementTheme.colors.textSecondary, maxLines = 1, @@ -134,11 +137,29 @@ private fun RowScope.SecondaryText( @PreviewsDayNight @Composable internal fun SenderNamePreview( - @PreviewParameter(SenderNameDataProvider::class) senderNameData: SenderNameData, + @PreviewParameter(SenderNameDataPreviewParam::class) senderNameData: SenderNameData, ) = ElementPreview { - SenderName( - senderId = senderNameData.userId, - senderProfile = senderNameData.profileDetails, - senderNameMode = senderNameData.senderNameMode, - ) + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + sequenceOf( + SenderNameMode.Timeline(mainColor = Color.Red), + SenderNameMode.Reply, + SenderNameMode.ActionList, + ).forEach { mode -> + Column { + Text( + "Rendering in mode ${mode::class.simpleName}:", + style = ElementTheme.typography.fontBodyXsRegular, + color = ElementTheme.colors.textSecondary, + ) + SenderName( + senderId = senderNameData.userId, + senderProfile = senderNameData.profileDetails, + senderNameMode = mode, + ) + } + } + } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderNameDataPreviewParam.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderNameDataPreviewParam.kt new file mode 100644 index 0000000000..103cb6a4f0 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderNameDataPreviewParam.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * Copyright 2024, 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.libraries.matrix.ui.messages.sender + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails +import io.element.android.libraries.matrix.api.user.DisplayedStatus +import io.element.android.libraries.matrix.api.user.UserStatus +import io.element.android.libraries.matrix.ui.messages.reply.aProfileDetailsReady + +data class SenderNameData( + val userId: UserId, + val profileDetails: ProfileDetails, +) + +open class SenderNameDataPreviewParam : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aSenderNameData(), + aSenderNameData( + displayNameAmbiguous = true, + ), + aSenderNameData( + profileDetails = ProfileDetails.Unavailable, + ), + aSenderNameData( + displayName = null, + ), + aSenderNameData( + displayedStatus = DisplayedStatus.UserSet( + status = UserStatus( + emoji = "😀", + text = "Should not be rendered", + ), + ), + ), + aSenderNameData( + displayedStatus = DisplayedStatus.InCall(0L), + ), + aSenderNameData( + displayNameAmbiguous = true, + displayedStatus = DisplayedStatus.UserSet( + status = UserStatus( + emoji = "😀", + text = "Should not be rendered", + ), + ), + ), + ) +} + +private fun aSenderNameData( + userId: UserId = UserId("@alice:example.com"), + displayName: String? = "Alice", + displayNameAmbiguous: Boolean = false, + displayedStatus: DisplayedStatus? = null, + profileDetails: ProfileDetails = aProfileDetailsReady( + displayName = displayName, + displayNameAmbiguous = displayNameAmbiguous, + displayedStatus = displayedStatus, + ) +) = SenderNameData( + userId = userId, + profileDetails = profileDetails, +) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderNameDataProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderNameDataProvider.kt deleted file mode 100644 index e4ca73e69c..0000000000 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/sender/SenderNameDataProvider.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2024, 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.libraries.matrix.ui.messages.sender - -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails -import io.element.android.libraries.matrix.api.user.DisplayedStatus - -data class SenderNameData( - val userId: UserId, - val profileDetails: ProfileDetails, - val senderNameMode: SenderNameMode, -) - -open class SenderNameDataProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - SenderNameMode.Timeline(mainColor = Color.Red), - SenderNameMode.Reply, - SenderNameMode.ActionList, - ) - .flatMap { senderNameMode -> - sequenceOf( - aSenderNameData( - senderNameMode = senderNameMode, - ), - aSenderNameData( - senderNameMode = senderNameMode, - displayNameAmbiguous = true, - ), - SenderNameData( - senderNameMode = senderNameMode, - userId = UserId("@alice:${senderNameMode.javaClass.simpleName.lowercase()}"), - profileDetails = ProfileDetails.Unavailable, - ), - ) - } -} - -private fun aSenderNameData( - senderNameMode: SenderNameMode, - displayNameAmbiguous: Boolean = false, - displayedStatus: DisplayedStatus? = null, -) = SenderNameData( - userId = UserId("@alice:${senderNameMode.javaClass.simpleName.lowercase()}"), - profileDetails = ProfileDetails.Ready( - displayName = "Alice ${senderNameMode.javaClass.simpleName}", - displayNameAmbiguous = displayNameAmbiguous, - avatarUrl = null, - displayedStatus = displayedStatus, - ), - senderNameMode = senderNameMode, -) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/LoadingRoomState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/LoadingRoomState.kt index cbbe79b480..cdd1475641 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/LoadingRoomState.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/LoadingRoomState.kt @@ -30,7 +30,7 @@ sealed interface LoadingRoomState { data class Loaded(val room: JoinedRoom) : LoadingRoomState } -open class LoadingRoomStateProvider : PreviewParameterProvider { +open class LoadingRoomStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( LoadingRoomState.Loading, diff --git a/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/SimplePlayer.kt b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/SimplePlayer.kt index 98ebc5ce50..9bec86e72f 100644 --- a/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/SimplePlayer.kt +++ b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/SimplePlayer.kt @@ -8,16 +8,8 @@ package io.element.android.libraries.mediaplayer.impl -import android.content.Context -import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Player -import androidx.media3.exoplayer.ExoPlayer -import dev.zacsweers.metro.BindingContainer -import dev.zacsweers.metro.ContributesTo -import dev.zacsweers.metro.Provides -import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.di.annotations.ApplicationContext /** * A subset of media3 [Player] that only exposes the few methods we need making it easier to mock. @@ -44,15 +36,6 @@ interface SimplePlayer { } } -@ContributesTo(RoomScope::class) -@BindingContainer -object SimplePlayerModule { - @Provides - fun simplePlayerProvider( - @ApplicationContext context: Context, - ): SimplePlayer = DefaultSimplePlayer(ExoPlayer.Builder(context).setWakeMode(C.WAKE_MODE_LOCAL).build()) -} - /** * Default implementation of [SimplePlayer] backed by a media3 [Player]. */ diff --git a/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/di/SimplePlayerBindingContainer.kt b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/di/SimplePlayerBindingContainer.kt new file mode 100644 index 0000000000..85518e58fd --- /dev/null +++ b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/di/SimplePlayerBindingContainer.kt @@ -0,0 +1,28 @@ +/* + * 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.libraries.mediaplayer.impl.di + +import android.content.Context +import androidx.media3.common.C +import androidx.media3.exoplayer.ExoPlayer +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Provides +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.di.annotations.ApplicationContext +import io.element.android.libraries.mediaplayer.impl.DefaultSimplePlayer +import io.element.android.libraries.mediaplayer.impl.SimplePlayer + +@ContributesTo(RoomScope::class) +@BindingContainer +object SimplePlayerBindingContainer { + @Provides + fun providesSimplePlayer( + @ApplicationContext context: Context, + ): SimplePlayer = DefaultSimplePlayer(ExoPlayer.Builder(context).setWakeMode(C.WAKE_MODE_LOCAL).build()) +} diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt index b374e530f4..36c5208603 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt @@ -26,6 +26,7 @@ import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.core.mimetype.MimeTypes.ensureDefaultSubtype import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo @@ -62,10 +63,10 @@ class AndroidMediaPreProcessor( /** * Used for calculating `inSampleSize` for bitmaps. * - * *Note*: Ideally, this should result in images of up to (but not included) 1280x1280 being sent. However, images with very different width and height + * *Note*: Ideally, this should result in images of up to (but not included) 2560x2560 being sent. However, images with very different width and height * values may surpass this limit. (i.e.: an image of `480x3000px` would have `inSampleSize=1` and be sent as is). */ - private const val IMAGE_SCALE_REF_SIZE = 640 + private const val IMAGE_SCALE_REF_SIZE = 1280 private val notCompressibleImageTypes = listOf(MimeTypes.Gif, MimeTypes.WebP) } @@ -82,15 +83,17 @@ class AndroidMediaPreProcessor( mediaOptimizationConfig: MediaOptimizationConfig, ): Result = withContext(coroutineDispatchers.computation) { runCatchingExceptions { + val resolvedMimeType = mimeType.ensureDefaultSubtype() val result = when { - mimeType == MimeTypes.Svg -> processSvgImage(uri, mimeType) - mimeType.isMimeTypeImage() -> { - val shouldBeCompressed = mediaOptimizationConfig.compressImages && mimeType !in notCompressibleImageTypes - processImage(uri, mimeType, shouldBeCompressed) + resolvedMimeType == MimeTypes.Svg -> processSvgImage(uri, resolvedMimeType) + resolvedMimeType.isMimeTypeImage() -> { + val imageMimeType = resolveImageMimeType(uri, mimeType).ensureDefaultSubtype() + val shouldBeCompressed = mediaOptimizationConfig.compressImages && imageMimeType !in notCompressibleImageTypes + processImage(uri, imageMimeType, shouldBeCompressed) } - mimeType.isMimeTypeVideo() -> processVideo(uri, mimeType, mediaOptimizationConfig.videoCompressionPreset) - mimeType.isMimeTypeAudio() -> processAudio(uri, mimeType) - else -> processFile(uri, mimeType) + resolvedMimeType.isMimeTypeVideo() -> processVideo(uri, resolvedMimeType, mediaOptimizationConfig.videoCompressionPreset) + resolvedMimeType.isMimeTypeAudio() -> processAudio(uri, resolvedMimeType) + else -> processFile(uri, resolvedMimeType) } if (deleteOriginal) { tryOrNull { @@ -155,6 +158,19 @@ class AndroidMediaPreProcessor( } } + /** + * The declared mime type can be broader than the image actually is - a wildcard from a share intent, or a mislabelled file - and the encoder is picked + * from it. Re-encoding a transparent image as JPEG flattens its alpha channel to black, so resolve the real format from the image header first. + */ + private fun resolveImageMimeType(uri: Uri, declaredMimeType: String): String { + if (declaredMimeType == MimeTypes.Png || declaredMimeType == MimeTypes.Jpeg) return declaredMimeType + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + tryOrNull { + contentResolver.openInputStream(uri)?.use { BitmapFactory.decodeStream(it, null, options) } + } + return options.outMimeType ?: declaredMimeType + } + private suspend fun processImage(uri: Uri, mimeType: String, shouldBeCompressed: Boolean): MediaUploadInfo { Timber.d("Processing image ${uri.path.orEmpty().hash()}") diff --git a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt index 3fb639aa9b..090cf11674 100644 --- a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt +++ b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt @@ -65,11 +65,26 @@ class AndroidMediaPreProcessorTest : RobolectricTest() { assertThat(height).isEqualTo(2268) } + @Test + fun `test processing an image shared with a wildcard mime type`() = runTest { + val mediaUploadInfo = process( + asset = assetImageJpeg, + mimeType = MimeTypes.Images, + mediaOptimizationConfig = MediaOptimizationConfig( + compressImages = true, + videoCompressionPreset = VideoCompressionPreset.STANDARD, + ), + ) + val info = mediaUploadInfo as MediaUploadInfo.Image + assertThat(info.imageInfo.mimetype).isEqualTo(MimeTypes.Jpeg) + } + private suspend fun TestScope.process( asset: Asset, mediaOptimizationConfig: MediaOptimizationConfig, sdkIntVersion: Int = Build.VERSION_CODES.P, deleteOriginal: Boolean = false, + mimeType: String = asset.mimeType, ): MediaUploadInfo { val context = InstrumentationRegistry.getInstrumentation().context val deleteCallback = lambdaRecorder {} @@ -81,7 +96,7 @@ class AndroidMediaPreProcessorTest : RobolectricTest() { val file = getFileFromAssets(context, asset.filename) val result = sut.process( uri = file.toUri(), - mimeType = asset.mimeType, + mimeType = mimeType, deleteOriginal = deleteOriginal, mediaOptimizationConfig = mediaOptimizationConfig, ) @@ -91,6 +106,52 @@ class AndroidMediaPreProcessorTest : RobolectricTest() { return data } + @Test + fun `a png shared with a wildcard mime type is still encoded as a png`() = runTest { + val mediaUploadInfo = process( + asset = assetImagePng, + mimeType = MimeTypes.Images, + mediaOptimizationConfig = MediaOptimizationConfig( + compressImages = true, + videoCompressionPreset = VideoCompressionPreset.STANDARD, + ), + sdkIntVersion = Build.VERSION_CODES.Q, + ) + val info = mediaUploadInfo as MediaUploadInfo.Image + assertThat(info.imageInfo.mimetype).isEqualTo(MimeTypes.Png) + } + + @Test + fun `a gif shared with a wildcard mime type is recognised and left uncompressed`() = runTest { + val mediaUploadInfo = process( + asset = assetAnimatedGif, + mimeType = MimeTypes.Images, + mediaOptimizationConfig = MediaOptimizationConfig( + compressImages = true, + videoCompressionPreset = VideoCompressionPreset.STANDARD, + ), + sdkIntVersion = Build.VERSION_CODES.Q, + ) + val info = mediaUploadInfo as MediaUploadInfo.Image + assertThat(info.imageInfo.mimetype).isEqualTo(MimeTypes.Gif) + assertThat(info.imageInfo.size).isEqualTo(assetAnimatedGif.size) + } + + @Test + fun `a file that is not an image falls back to the default subtype of the wildcard it was shared with`() = runTest { + val mediaUploadInfo = process( + asset = assetText, + mimeType = MimeTypes.Images, + mediaOptimizationConfig = MediaOptimizationConfig( + compressImages = false, + videoCompressionPreset = VideoCompressionPreset.STANDARD, + ), + sdkIntVersion = Build.VERSION_CODES.Q, + ) + val info = mediaUploadInfo as MediaUploadInfo.Image + assertThat(info.imageInfo.mimetype).isEqualTo(MimeTypes.Jpeg) + } + @Test @Ignore("Ignore now that min API for enterprise is 33") fun `test processing png`() = runTest { @@ -208,10 +269,10 @@ class AndroidMediaPreProcessorTest : RobolectricTest() { assertThat(info.thumbnailFile).isNotNull() assertThat(info.imageInfo).isEqualTo( ImageInfo( - height = 979, - width = 3006, + height = 1958, + width = 6012, mimetype = MimeTypes.Jpeg, - size = 84_845, + size = 682_492, ThumbnailInfo(height = 244, width = 751, mimetype = MimeTypes.Jpeg, size = 7_178), thumbnailSource = null, blurhash = "K07gBzX=j_D4xZjoaSe,s:" @@ -233,10 +294,10 @@ class AndroidMediaPreProcessorTest : RobolectricTest() { assertThat(info.thumbnailFile).isNull() assertThat(info.imageInfo).isEqualTo( ImageInfo( - height = 979, - width = 3_006, + height = 1_958, + width = 6_012, mimetype = MimeTypes.Jpeg, - size = 84_845, + size = 682_492, thumbnailInfo = null, thumbnailSource = null, blurhash = null, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDeleteConfirmationProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDeleteConfirmationPreviewParam.kt similarity index 90% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDeleteConfirmationProvider.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDeleteConfirmationPreviewParam.kt index 72b16c4e1d..449ab1c5c3 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDeleteConfirmationProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDeleteConfirmationPreviewParam.kt @@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.anImageMediaInfo -open class MediaBottomSheetStateDeleteConfirmationProvider : PreviewParameterProvider { +open class MediaBottomSheetStateDeleteConfirmationPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMediaBottomSheetStateDeleteConfirmation(), diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDetailsProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDetailsPreviewParam.kt similarity index 93% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDetailsProvider.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDetailsPreviewParam.kt index 7540d49b8a..a38c5c2ac9 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDetailsProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaBottomSheetStateDetailsPreviewParam.kt @@ -14,7 +14,7 @@ import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.anApkMediaInfo import io.element.android.libraries.mediaviewer.api.anImageMediaInfo -open class MediaBottomSheetStateDetailsProvider : PreviewParameterProvider { +open class MediaBottomSheetStateDetailsPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMediaBottomSheetStateDetails(), diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheet.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheet.kt index 38bbb4dcf0..6aff6cf136 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheet.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheet.kt @@ -168,7 +168,7 @@ private fun MediaRow( @PreviewsDayNight @Composable internal fun MediaDeleteConfirmationBottomSheetPreview( - @PreviewParameter(provider = MediaBottomSheetStateDeleteConfirmationProvider::class) state: MediaBottomSheetState.DeleteConfirmation, + @PreviewParameter(provider = MediaBottomSheetStateDeleteConfirmationPreviewParam::class) state: MediaBottomSheetState.DeleteConfirmation, ) = ElementPreview(fillMaxSize = true) { MediaDeleteConfirmationBottomSheet( state = state, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt index 35d056dc39..f4b35a78ff 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt @@ -266,7 +266,7 @@ private fun SectionText( @PreviewsDayNight @Composable internal fun MediaDetailsBottomSheetPreview( - @PreviewParameter(MediaBottomSheetStateDetailsProvider::class) state: MediaBottomSheetState.Details, + @PreviewParameter(MediaBottomSheetStateDetailsPreviewParam::class) state: MediaBottomSheetState.Details, ) = ElementPreview(fillMaxSize = true) { MediaDetailsBottomSheet( state = state, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStatePreviewParam.kt similarity index 98% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStatePreviewParam.kt index 43a361ac29..6d90fff26a 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStateProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryStatePreviewParam.kt @@ -28,7 +28,7 @@ import io.element.android.libraries.mediaviewer.impl.model.aMediaItemVideo import io.element.android.libraries.mediaviewer.impl.model.aMediaItemVoice import kotlinx.collections.immutable.toImmutableList -open class MediaGalleryStateProvider : PreviewParameterProvider { +open class MediaGalleryStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMediaGalleryState( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt index 441b6082dd..132fb4af91 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryView.kt @@ -597,7 +597,7 @@ private fun LoadingContent( @PreviewsDayNight @Composable internal fun MediaGalleryViewPreview( - @PreviewParameter(MediaGalleryStateProvider::class) state: MediaGalleryState + @PreviewParameter(MediaGalleryStatePreviewParam::class) state: MediaGalleryState ) = ElementPreview { CompositionLocalProvider( LocalMediaItemPresenterFactories provides aFakeMediaItemPresenterFactories(), diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt index b74debc177..94c5485130 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactories.kt @@ -10,28 +10,13 @@ package io.element.android.libraries.mediaviewer.impl.gallery.di import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import dev.zacsweers.metro.BindingContainer -import dev.zacsweers.metro.ContributesTo import dev.zacsweers.metro.Inject -import dev.zacsweers.metro.Multibinds import dev.zacsweers.metro.SingleIn import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope import io.element.android.libraries.mediaviewer.impl.model.MediaItem import kotlin.reflect.KClass -/** - * Container that declares the [MediaItemPresenterFactory] map multi binding. - * - * Its sole purpose is to support the case of an empty map multibinding. - */ -@BindingContainer -@ContributesTo(RoomScope::class) -interface MediaItemPresenterFactoriesModule { - @Multibinds - fun multiBindMediaItemPresenterFactories(): @JvmSuppressWildcards Map, MediaItemPresenterFactory<*, *>> -} - /** * Room level caching layer for the [MediaItemPresenterFactory] instances. * diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactoriesBindingContainer.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactoriesBindingContainer.kt new file mode 100644 index 0000000000..2ab533aeca --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/di/MediaItemPresenterFactoriesBindingContainer.kt @@ -0,0 +1,27 @@ +/* + * 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.libraries.mediaviewer.impl.gallery.di + +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.Multibinds +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.mediaviewer.impl.model.MediaItem +import kotlin.reflect.KClass + +/** + * Container that declares the [MediaItemPresenterFactory] map multi binding. + * + * Its sole purpose is to support the case of an empty map multibinding. + */ +@BindingContainer +@ContributesTo(RoomScope::class) +interface MediaItemPresenterFactoriesBindingContainer { + @Multibinds + fun multiBindMediaItemPresenterFactories(): @JvmSuppressWildcards Map, MediaItemPresenterFactory<*, *>> +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt index cda8422e33..36d1c2de35 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/AudioItemView.kt @@ -46,7 +46,7 @@ fun AudioItemView( @PreviewsDayNight @Composable internal fun AudioItemViewPreview( - @PreviewParameter(MediaItemAudioProvider::class) audio: MediaItem.Audio, + @PreviewParameter(MediaItemAudioPreviewParam::class) audio: MediaItem.Audio, ) = ElementPreview { val states = remember { listOf( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/DateItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/DateItemView.kt index b04b25a53e..8f86c86523 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/DateItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/DateItemView.kt @@ -45,7 +45,7 @@ fun DateItemView( @PreviewsDayNight @Composable internal fun DateItemViewPreview( - @PreviewParameter(MediaItemDateSeparatorProvider::class) date: MediaItem.DateSeparator, + @PreviewParameter(MediaItemDateSeparatorPreviewParam::class) date: MediaItem.DateSeparator, ) = ElementPreview { DateItemView(date) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt index b552c374b9..1919eea459 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/FileItemView.kt @@ -46,7 +46,7 @@ fun FileItemView( @PreviewsDayNight @Composable internal fun FileItemViewPreview( - @PreviewParameter(MediaItemFileProvider::class) file: MediaItem.File, + @PreviewParameter(MediaItemFilePreviewParam::class) file: MediaItem.File, ) = ElementPreview { val states = remember { listOf( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemAudioProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemAudioPreviewParam.kt similarity index 92% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemAudioProvider.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemAudioPreviewParam.kt index 4dfd1c18c8..7d5d39c9bd 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemAudioProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemAudioPreviewParam.kt @@ -13,7 +13,7 @@ import io.element.android.libraries.core.preview.loremIpsum import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.mediaviewer.impl.model.aMediaItemAudio -class MediaItemAudioProvider : PreviewParameterProvider { +class MediaItemAudioPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMediaItemAudio(), diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemDateSeparatorProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemDateSeparatorPreviewParam.kt similarity index 88% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemDateSeparatorProvider.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemDateSeparatorPreviewParam.kt index 05705a9251..914df563cf 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemDateSeparatorProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemDateSeparatorPreviewParam.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.mediaviewer.impl.model.aMediaItemDateSeparator -class MediaItemDateSeparatorProvider : PreviewParameterProvider { +class MediaItemDateSeparatorPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMediaItemDateSeparator(), diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemFileProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemFilePreviewParam.kt similarity index 92% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemFileProvider.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemFilePreviewParam.kt index e64b10adb4..f0bd4c4383 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemFileProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemFilePreviewParam.kt @@ -13,7 +13,7 @@ import io.element.android.libraries.core.preview.loremIpsum import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.mediaviewer.impl.model.aMediaItemFile -class MediaItemFileProvider : PreviewParameterProvider { +class MediaItemFilePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMediaItemFile(), diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVideoProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVideoPreviewParam.kt similarity index 90% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVideoProvider.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVideoPreviewParam.kt index c9c4666904..e3e4017e8d 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVideoProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVideoPreviewParam.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.mediaviewer.impl.model.aMediaItemVideo -class MediaItemVideoProvider : PreviewParameterProvider { +class MediaItemVideoPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMediaItemVideo(), diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVoiceProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVoicePreviewParam.kt similarity index 92% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVoiceProvider.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVoicePreviewParam.kt index 56c1c4bb32..4f0ec2f3fa 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVoiceProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/MediaItemVoicePreviewParam.kt @@ -13,7 +13,7 @@ import io.element.android.libraries.core.preview.loremIpsum import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.mediaviewer.impl.model.aMediaItemVoice -class MediaItemVoiceProvider : PreviewParameterProvider { +class MediaItemVoicePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMediaItemVoice(), diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt index 3aad3f1061..01d1e29f08 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt @@ -117,7 +117,7 @@ private fun VideoInfoRow( @PreviewsDayNight @Composable internal fun VideoItemViewPreview( - @PreviewParameter(MediaItemVideoProvider::class) video: MediaItem.Video, + @PreviewParameter(MediaItemVideoPreviewParam::class) video: MediaItem.Video, ) = ElementPreview { VideoItemView( video = video, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt index 6c9df5fc48..2ffaed7af0 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VoiceItemView.kt @@ -54,7 +54,7 @@ import io.element.android.libraries.mediaviewer.impl.model.aMediaItemVoice import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.voiceplayer.api.VoiceMessageEvent import io.element.android.libraries.voiceplayer.api.VoiceMessageState -import io.element.android.libraries.voiceplayer.api.VoiceMessageStateProvider +import io.element.android.libraries.voiceplayer.api.VoiceMessageStatePreviewParam import io.element.android.libraries.voiceplayer.api.aVoiceMessageState import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay @@ -279,7 +279,7 @@ private fun CustomIconButton( @PreviewsDayNight @Composable internal fun VoiceItemViewPreview( - @PreviewParameter(MediaItemVoiceProvider::class) voice: MediaItem.Voice, + @PreviewParameter(MediaItemVoicePreviewParam::class) voice: MediaItem.Voice, ) = ElementPreview { val states = remember { listOf( @@ -304,7 +304,7 @@ internal fun VoiceItemViewPreview( @PreviewsDayNight @Composable internal fun VoiceItemViewPlayPreview( - @PreviewParameter(VoiceMessageStateProvider::class) state: VoiceMessageState, + @PreviewParameter(VoiceMessageStatePreviewParam::class) state: VoiceMessageState, ) = ElementPreview { val validationState = remember { listOf( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt index 32a9b0f74c..2910d28936 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/VoiceMessagePresenter.kt @@ -12,28 +12,13 @@ import androidx.compose.runtime.Composable import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject -import dev.zacsweers.metro.BindingContainer -import dev.zacsweers.metro.Binds -import dev.zacsweers.metro.ContributesTo -import dev.zacsweers.metro.IntoMap import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.mediaviewer.impl.gallery.di.MediaItemEventContentKey import io.element.android.libraries.mediaviewer.impl.gallery.di.MediaItemPresenterFactory import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.voiceplayer.api.VoiceMessagePresenterFactory import io.element.android.libraries.voiceplayer.api.VoiceMessageState import kotlin.time.Duration -@BindingContainer -@ContributesTo(RoomScope::class) -interface VoiceMessagePresenterModule { - @Binds - @IntoMap - @MediaItemEventContentKey(MediaItem.Voice::class) - fun bindVoiceMessagePresenterFactory(factory: VoiceMessagePresenter.Factory): MediaItemPresenterFactory<*, *> -} - @AssistedInject class VoiceMessagePresenter( voiceMessagePresenterFactory: VoiceMessagePresenterFactory, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/di/VoiceMessagePresenterBindingContainer.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/di/VoiceMessagePresenterBindingContainer.kt new file mode 100644 index 0000000000..384f48367a --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/voice/di/VoiceMessagePresenterBindingContainer.kt @@ -0,0 +1,27 @@ +/* + * 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.libraries.mediaviewer.impl.gallery.voice.di + +import dev.zacsweers.metro.BindingContainer +import dev.zacsweers.metro.Binds +import dev.zacsweers.metro.ContributesTo +import dev.zacsweers.metro.IntoMap +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.mediaviewer.impl.gallery.di.MediaItemEventContentKey +import io.element.android.libraries.mediaviewer.impl.gallery.di.MediaItemPresenterFactory +import io.element.android.libraries.mediaviewer.impl.gallery.voice.VoiceMessagePresenter +import io.element.android.libraries.mediaviewer.impl.model.MediaItem + +@BindingContainer +@ContributesTo(RoomScope::class) +interface VoiceMessagePresenterBindingContainer { + @Binds + @IntoMap + @MediaItemEventContentKey(MediaItem.Voice::class) + fun bindVoiceMessagePresenterFactory(factory: VoiceMessagePresenter.Factory): MediaItemPresenterFactory<*, *> +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaAudioView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaAudioView.kt index 3ac578196d..4ef746cb89 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaAudioView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaAudioView.kt @@ -393,7 +393,7 @@ private fun AudioInfoView( @PreviewsDayNight @Composable internal fun MediaAudioViewPreview( - @PreviewParameter(MediaInfoAudioProvider::class) info: MediaInfo + @PreviewParameter(MediaInfoAudioPreviewParam::class) info: MediaInfo ) = ElementPreview { MediaAudioView( modifier = Modifier.fillMaxSize(), diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaInfoAudioProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaInfoAudioPreviewParam.kt similarity index 91% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaInfoAudioProvider.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaInfoAudioPreviewParam.kt index 02607e3e0e..900321a909 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaInfoAudioProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/audio/MediaInfoAudioPreviewParam.kt @@ -13,7 +13,7 @@ import io.element.android.libraries.designsystem.components.media.WaveFormSample import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo -open class MediaInfoAudioProvider : PreviewParameterProvider { +open class MediaInfoAudioPreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( anAudioMediaInfo(), diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaFileView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaFileView.kt index a1c57ef272..89e02b616a 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaFileView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaFileView.kt @@ -114,7 +114,7 @@ fun MediaFileView( @PreviewsDayNight @Composable internal fun MediaFileViewPreview( - @PreviewParameter(MediaInfoFileProvider::class) info: MediaInfo + @PreviewParameter(MediaInfoFilePreviewParam::class) info: MediaInfo ) = ElementPreview { MediaFileView( modifier = Modifier.fillMaxSize(), diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaInfoFileProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaInfoFilePreviewParam.kt similarity index 91% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaInfoFileProvider.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaInfoFilePreviewParam.kt index 3214236253..b647eb8fd2 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaInfoFileProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/file/MediaInfoFilePreviewParam.kt @@ -14,7 +14,7 @@ import io.element.android.libraries.mediaviewer.api.aZipMediaInfo import io.element.android.libraries.mediaviewer.api.anApkMediaInfo import io.element.android.libraries.mediaviewer.api.anAudioMediaInfo -open class MediaInfoFileProvider : PreviewParameterProvider { +open class MediaInfoFilePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aZipMediaInfo(), diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerForPreview.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerForPreview.kt index 4da8433cd1..e23cb790db 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerForPreview.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/ExoPlayerForPreview.kt @@ -266,4 +266,5 @@ class ExoPlayerForPreview( override fun setVideoCodecParameters(codecParameters: CodecParameters) {} override fun addVideoCodecParametersChangeListener(listener: CodecParametersChangeListener, keys: List) {} override fun removeVideoCodecParametersChangeListener(listener: CodecParametersChangeListener) {} + override fun setEnforceAdPlaybackOnTimelineRefresh(enforceAdPlaybackOnTimelineRefresh: Boolean) = throw NotImplementedError() } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerStateProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerStatePreviewParam.kt similarity index 92% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerStateProvider.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerStatePreviewParam.kt index 55432fb817..b229d11283 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerStateProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerStatePreviewParam.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.mediaviewer.impl.local.player import androidx.compose.ui.tooling.preview.PreviewParameterProvider -open class MediaPlayerControllerStateProvider : PreviewParameterProvider { +open class MediaPlayerControllerStatePreviewParam : PreviewParameterProvider { override val values: Sequence = sequenceOf( aMediaPlayerControllerState(), aMediaPlayerControllerState( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerView.kt index 1f2b0b93d2..0a68a2d75d 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/player/MediaPlayerControllerView.kt @@ -213,7 +213,7 @@ fun MediaPlayerControllerView( @PreviewsDayNight @Composable internal fun MediaPlayerControllerViewPreview( - @PreviewParameter(MediaPlayerControllerStateProvider::class) state: MediaPlayerControllerState + @PreviewParameter(MediaPlayerControllerStatePreviewParam::class) state: MediaPlayerControllerState ) = ElementPreview { MediaPlayerControllerView( state = state, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileContentProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileContentPreviewParam.kt similarity index 89% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileContentProvider.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileContentPreviewParam.kt index d92e400495..108f8dd066 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileContentProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileContentPreviewParam.kt @@ -13,7 +13,7 @@ import io.element.android.libraries.architecture.AsyncData import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -open class TextFileContentProvider : PreviewParameterProvider>> { +open class TextFileContentPreviewParam : PreviewParameterProvider>> { override val values: Sequence>> get() = sequenceOf( AsyncData.Uninitialized, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileView.kt index f4984470dd..2f397103e8 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileView.kt @@ -98,7 +98,7 @@ private fun TextFileContentView( @PreviewsDayNight @Composable internal fun TextFileContentViewPreview( - @PreviewParameter(TextFileContentProvider::class) text: AsyncData>, + @PreviewParameter(TextFileContentPreviewParam::class) text: AsyncData>, ) = ElementPreview { TextFileContentView( data = text, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt index e53153ce19..77222d97ff 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt @@ -92,7 +92,7 @@ class MediaViewerNode( is MediaViewerEntryPoint.Params.RoomMedia -> { val eventId = inputs.eventId if (eventId == null) { - // Should not happen + // Local echo, the viewer will not be focused on the item timelineMediaGalleryDataSource } else { // Can we use a specific timeline? diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStatePreviewParam.kt similarity index 99% rename from libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt rename to libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStatePreviewParam.kt index 440d366c30..ea422e41f1 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStatePreviewParam.kt @@ -41,7 +41,7 @@ private const val LONG_CAPTION = "This is a very long caption that should be scr "Line 4: Duis aute irure dolor in reprehenderit in voluptate velit esse cillum. " + "Line 5: Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia." -open class MediaViewerStateProvider : PreviewParameterProvider { +open class MediaViewerStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMediaViewerState(), diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt index e1c1a0c250..343bf99ec9 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt @@ -749,7 +749,7 @@ private fun ErrorView( // Only preview in dark, dark theme is forced on the Node. @Preview @Composable -internal fun MediaViewerViewPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) = ElementPreviewDark { +internal fun MediaViewerViewPreview(@PreviewParameter(MediaViewerStatePreviewParam::class) state: MediaViewerState) = ElementPreviewDark { MediaViewerView( state = state, audioFocus = null, @@ -760,7 +760,7 @@ internal fun MediaViewerViewPreview(@PreviewParameter(MediaViewerStateProvider:: @Preview(device = "${Devices.PHONE}, orientation=landscape") @Composable -internal fun MediaViewerViewLandscapePreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) = ElementPreviewDark { +internal fun MediaViewerViewLandscapePreview(@PreviewParameter(MediaViewerStatePreviewParam::class) state: MediaViewerState) = ElementPreviewDark { MediaViewerView( state = state, audioFocus = null, diff --git a/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt b/libraries/network/src/main/kotlin/io/element/android/libraries/network/di/NetworkBindingContainer.kt similarity index 95% rename from libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt rename to libraries/network/src/main/kotlin/io/element/android/libraries/network/di/NetworkBindingContainer.kt index 90e78d860e..46f00a6b9b 100644 --- a/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt +++ b/libraries/network/src/main/kotlin/io/element/android/libraries/network/di/NetworkBindingContainer.kt @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.network +package io.element.android.libraries.network.di import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.BindingContainer @@ -23,7 +23,7 @@ import java.util.concurrent.TimeUnit @BindingContainer @ContributesTo(AppScope::class) -object NetworkModule { +object NetworkBindingContainer { @Provides @SingleIn(AppScope::class) fun providesOkHttpClient( diff --git a/libraries/network/src/main/kotlin/io/element/android/libraries/network/useragent/DefaultUserAgentProvider.kt b/libraries/network/src/main/kotlin/io/element/android/libraries/network/useragent/DefaultUserAgentProvider.kt index 3ecd091e0d..2a32c6d273 100644 --- a/libraries/network/src/main/kotlin/io/element/android/libraries/network/useragent/DefaultUserAgentProvider.kt +++ b/libraries/network/src/main/kotlin/io/element/android/libraries/network/useragent/DefaultUserAgentProvider.kt @@ -12,6 +12,7 @@ import android.os.Build import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.SingleIn +import io.element.android.libraries.core.extensions.normalized import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.SdkMetadata @@ -26,11 +27,11 @@ class DefaultUserAgentProvider( override fun provide(): String = userAgent /** - * Create an user agent with the application version. + * Create a user agent with the application version. * Ex: Element X/1.5.0 (Xiaomi Mi 9T; Android 11; RKQ1.200826.002; Sdk c344b155c) */ private fun buildUserAgent(): String { - val appName = buildMeta.applicationName + val appName = buildMeta.applicationName.normalized() val appVersion = buildMeta.versionName val deviceManufacturer = Build.MANUFACTURER val deviceModel = Build.MODEL diff --git a/libraries/oauth/test/src/main/kotlin/io/element/android/libraries/oauth/test/customtab/FakeOAuthActionFlow.kt b/libraries/oauth/test/src/main/kotlin/io/element/android/libraries/oauth/test/FakeOAuthActionFlow.kt similarity index 94% rename from libraries/oauth/test/src/main/kotlin/io/element/android/libraries/oauth/test/customtab/FakeOAuthActionFlow.kt rename to libraries/oauth/test/src/main/kotlin/io/element/android/libraries/oauth/test/FakeOAuthActionFlow.kt index 5a5ca4369e..49abe58622 100644 --- a/libraries/oauth/test/src/main/kotlin/io/element/android/libraries/oauth/test/customtab/FakeOAuthActionFlow.kt +++ b/libraries/oauth/test/src/main/kotlin/io/element/android/libraries/oauth/test/FakeOAuthActionFlow.kt @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.oauth.test.customtab +package io.element.android.libraries.oauth.test import io.element.android.libraries.oauth.api.OAuthAction import io.element.android.libraries.oauth.api.OAuthActionFlow diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStateProvider.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStatePreviewParam.kt similarity index 93% rename from libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStateProvider.kt rename to libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStatePreviewParam.kt index a330333d17..9052667578 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStateProvider.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsStatePreviewParam.kt @@ -11,7 +11,7 @@ package io.element.android.libraries.permissions.api import android.Manifest import androidx.compose.ui.tooling.preview.PreviewParameterProvider -open class PermissionsStateProvider : PreviewParameterProvider { +open class PermissionsStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aPermissionsState(showDialog = true, permission = Manifest.permission.POST_NOTIFICATIONS), diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt index af0f3dd5e8..7a641b1cfd 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt @@ -55,7 +55,7 @@ private fun String.toDialogContent(): String { @PreviewsDayNight @Composable -internal fun PermissionsViewPreview(@PreviewParameter(PermissionsStateProvider::class) state: PermissionsState) = ElementPreview { +internal fun PermissionsViewPreview(@PreviewParameter(PermissionsStatePreviewParam::class) state: PermissionsState) = ElementPreview { PermissionsView( state = state, ) diff --git a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt index f35fa88442..6285c46389 100644 --- a/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt +++ b/libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/AppPreferencesStore.kt @@ -84,6 +84,20 @@ interface AppPreferencesStore { /** The configured log level; defaults to the one the build was compiled with. */ fun getTracingLogLevelFlow(): Flow + /** + * The list of account providers (homeserver URLs) the user has previously authenticated + * against, most recent first. Used to default the account provider and to power autocomplete + * suggestions during sign-in. Stored locally only; never synced across devices. + */ + fun getHomeserverHistoryFlow(): Flow> + + /** + * Add [url] to the front of the account provider history (see [getHomeserverHistoryFlow]). + * The value is normalised (trimmed + lowercased); existing case-insensitive duplicates are + * moved to the front rather than duplicated, and the list is capped in size. No-op for blanks. + */ + suspend fun addHomeserverToHistory(url: String) + /** * @param targets the extra log packs to enable on top of the default targets. */ diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt index af35ac3c44..1a14a0ea8f 100644 --- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.preferences.impl.store +import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey @@ -36,6 +37,7 @@ private val timelineMediaPreviewValueKey = stringPreferencesKey("timelineMediaPr private val liveLocationMinimumDistanceUpdateKey = intPreferencesKey("liveLocationMinimumDistanceUpdate") private val logLevelKey = stringPreferencesKey("logLevel") private val traceLogPacksKey = stringPreferencesKey("traceLogPacks") +private val homeserverHistoryKey = stringPreferencesKey("homeserverHistory") private val messageSoundUriKey = stringPreferencesKey("notificationMessageSoundUri") private val messageSoundChannelVersionKey = intPreferencesKey("notificationMessageSoundChannelVersion") private val messageSoundDisplayNameKey = stringPreferencesKey("notificationMessageSoundDisplayName") @@ -43,6 +45,10 @@ private val callRingtoneUriKey = stringPreferencesKey("notificationCallRingtoneU private val callRingtoneChannelVersionKey = intPreferencesKey("notificationCallRingtoneChannelVersion") private val callRingtoneDisplayNameKey = stringPreferencesKey("notificationCallRingtoneDisplayName") +// URLs never contain a newline, so it is a safe delimiter to persist an ordered list in a single String. +private const val HOMESERVER_HISTORY_DELIMITER = "\n" +private const val MAX_HOMESERVER_HISTORY_SIZE = 20 + @ContributesBinding(AppScope::class) class DefaultAppPreferencesStore( private val buildMeta: BuildMeta, @@ -168,6 +174,20 @@ class DefaultAppPreferencesStore( } } + override fun getHomeserverHistoryFlow(): Flow> { + return store.data.map { prefs -> prefs.readHomeserverHistory() } + } + + override suspend fun addHomeserverToHistory(url: String) { + val normalized = url.trim().lowercase() + if (normalized.isEmpty()) return + store.edit { prefs -> + val updated = (listOf(normalized) + prefs.readHomeserverHistory().filter { it != normalized }) + .take(MAX_HOMESERVER_HISTORY_SIZE) + prefs[homeserverHistoryKey] = updated.joinToString(HOMESERVER_HISTORY_DELIMITER) + } + } + override fun getMessageSoundFlow(): Flow { return store.data.map { prefs -> NotificationSound.fromStored(prefs[messageSoundUriKey]) } } @@ -242,6 +262,13 @@ class DefaultAppPreferencesStore( } } +private fun Preferences.readHomeserverHistory(): List { + return this[homeserverHistoryKey] + ?.split(HOMESERVER_HISTORY_DELIMITER) + ?.filter { it.isNotEmpty() } + ?: emptyList() +} + private fun BuildMeta.defaultLogLevel(): LogLevel { return when (buildType) { BuildType.DEBUG -> LogLevel.TRACE diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/SessionPreferencesModule.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/di/SessionPreferencesBindingContainer.kt similarity index 84% rename from libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/SessionPreferencesModule.kt rename to libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/di/SessionPreferencesBindingContainer.kt index 43de0b5a62..396c3c7bc0 100644 --- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/SessionPreferencesModule.kt +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/di/SessionPreferencesBindingContainer.kt @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.preferences.impl.store +package io.element.android.libraries.preferences.impl.store.di import dev.zacsweers.metro.BindingContainer import dev.zacsweers.metro.ContributesTo @@ -15,11 +15,12 @@ import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.preferences.api.store.SessionPreferencesStore +import io.element.android.libraries.preferences.impl.store.DefaultSessionPreferencesStoreFactory import kotlinx.coroutines.CoroutineScope @BindingContainer @ContributesTo(SessionScope::class) -object SessionPreferencesModule { +object SessionPreferencesBindingContainer { @Provides fun providesSessionPreferencesStore( defaultSessionPreferencesStoreFactory: DefaultSessionPreferencesStoreFactory, diff --git a/libraries/preferences/impl/src/test/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStoreTest.kt b/libraries/preferences/impl/src/test/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStoreTest.kt index c52d1648ac..4cf018371a 100644 --- a/libraries/preferences/impl/src/test/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStoreTest.kt +++ b/libraries/preferences/impl/src/test/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStoreTest.kt @@ -54,4 +54,74 @@ class DefaultAppPreferencesStoreTest { assertThat(store.getLiveLocationMinimumDistanceInMetersUpdateFlow().first()).isEqualTo(25) } + + @Test + fun `homeserver history is empty by default`() = runTest { + val store = createStore() + assertThat(store.getHomeserverHistoryFlow().first()).isEmpty() + } + + @Test + fun `adding homeservers keeps the most recent first`() = runTest { + val store = createStore() + + store.addHomeserverToHistory("https://matrix.org") + store.addHomeserverToHistory("https://example.com") + + assertThat(store.getHomeserverHistoryFlow().first()) + .containsExactly("https://example.com", "https://matrix.org") + .inOrder() + } + + @Test + fun `adding an existing homeserver moves it to the front without duplicating`() = runTest { + val store = createStore() + + store.addHomeserverToHistory("https://matrix.org") + store.addHomeserverToHistory("https://example.com") + store.addHomeserverToHistory("https://matrix.org") + + assertThat(store.getHomeserverHistoryFlow().first()) + .containsExactly("https://matrix.org", "https://example.com") + .inOrder() + } + + @Test + fun `homeservers are normalised and deduplicated case-insensitively`() = runTest { + val store = createStore() + + store.addHomeserverToHistory(" HTTPS://Matrix.ORG ") + store.addHomeserverToHistory("https://matrix.org") + + assertThat(store.getHomeserverHistoryFlow().first()) + .containsExactly("https://matrix.org") + } + + @Test + fun `blank homeservers are ignored`() = runTest { + val store = createStore() + + store.addHomeserverToHistory(" ") + + assertThat(store.getHomeserverHistoryFlow().first()).isEmpty() + } + + @Test + fun `homeserver history is capped to the most recent entries`() = runTest { + val store = createStore() + + repeat(25) { index -> + store.addHomeserverToHistory("https://server$index.org") + } + + val history = store.getHomeserverHistoryFlow().first() + assertThat(history).hasSize(20) + assertThat(history.first()).isEqualTo("https://server24.org") + assertThat(history.last()).isEqualTo("https://server5.org") + } + + private fun createStore() = DefaultAppPreferencesStore( + buildMeta = buildMeta, + preferenceDataStoreFactory = FakePreferenceDataStoreFactory(), + ) } diff --git a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt index 6499e70223..e8d0b0bb8e 100644 --- a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt +++ b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemoryAppPreferencesStore.kt @@ -16,6 +16,7 @@ import io.element.android.libraries.preferences.api.store.NotificationSound import io.element.android.libraries.preferences.api.store.NotificationSoundChannelConfig import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet class InMemoryAppPreferencesStore( @@ -27,6 +28,7 @@ class InMemoryAppPreferencesStore( liveLocationMinimumDistanceUpdate: Int = 10, logLevel: LogLevel = LogLevel.INFO, traceLogPacks: Set = emptySet(), + homeserverHistory: List = emptyList(), messageSound: NotificationSound = NotificationSound.SystemDefault, messageSoundChannelVersion: Int = 0, messageSoundDisplayName: String? = null, @@ -40,6 +42,7 @@ class InMemoryAppPreferencesStore( private val liveLocationMinimumDistanceUpdate = MutableStateFlow(liveLocationMinimumDistanceUpdate) private val logLevel = MutableStateFlow(logLevel) private val tracingLogPacks = MutableStateFlow(traceLogPacks) + private val homeserverHistory = MutableStateFlow(homeserverHistory) private val hideInviteAvatars = MutableStateFlow(hideInviteAvatars) private val timelineMediaPreviewValue = MutableStateFlow(timelineMediaPreviewValue) private val messageSound = MutableStateFlow(messageSound) @@ -117,6 +120,18 @@ class InMemoryAppPreferencesStore( return tracingLogPacks } + override fun getHomeserverHistoryFlow(): Flow> { + return homeserverHistory + } + + override suspend fun addHomeserverToHistory(url: String) { + val normalized = url.trim().lowercase() + if (normalized.isEmpty()) return + homeserverHistory.update { current -> + (listOf(normalized) + current.filter { it != normalized }).take(10) + } + } + override fun getMessageSoundFlow(): Flow { return messageSound } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationEvents.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationEvent.kt similarity index 77% rename from libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationEvents.kt rename to libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationEvent.kt index b7a01cc61f..56e6a9caa7 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationEvents.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationEvent.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.push.api.battery -sealed interface BatteryOptimizationEvents { - data object Dismiss : BatteryOptimizationEvents - data object RequestDisableOptimizations : BatteryOptimizationEvents +sealed interface BatteryOptimizationEvent { + data object Dismiss : BatteryOptimizationEvent + data object RequestDisableOptimizations : BatteryOptimizationEvent } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationState.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationState.kt index 6732f7c17d..6a938eaec1 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationState.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationState.kt @@ -10,5 +10,5 @@ package io.element.android.libraries.push.api.battery data class BatteryOptimizationState( val shouldDisplayBanner: Boolean, - val eventSink: (BatteryOptimizationEvents) -> Unit, + val eventSink: (BatteryOptimizationEvent) -> Unit, ) diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationStateProvider.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationStateProvider.kt index 684185f40a..66531fb13b 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationStateProvider.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/battery/BatteryOptimizationStateProvider.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.push.api.battery fun aBatteryOptimizationState( shouldDisplayBanner: Boolean = false, - eventSink: (BatteryOptimizationEvents) -> Unit = {}, + eventSink: (BatteryOptimizationEvent) -> Unit = {}, ) = BatteryOptimizationState( shouldDisplayBanner = shouldDisplayBanner, eventSink = eventSink, diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index b9082c50f6..8615485bba 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -1,3 +1,5 @@ +import config.BuildTimeConfig +import extension.buildConfigFieldStr import extension.setupDependencyInjection import extension.testCommonDependencies @@ -17,6 +19,33 @@ plugins { android { namespace = "io.element.android.libraries.push.impl" + buildFeatures { + buildConfig = true + } + + buildTypes { + val defaultPusherAppId = "im.vector.app.android" + getByName("release") { + buildConfigFieldStr( + name = "PUSHER_APP_ID", + value = BuildTimeConfig.PUSHER_APP_ID_RELEASE ?: defaultPusherAppId, + ) + } + getByName("debug") { + buildConfigFieldStr( + name = "PUSHER_APP_ID", + value = BuildTimeConfig.PUSHER_APP_ID_DEBUG ?: defaultPusherAppId, + ) + } + register("nightly") { + matchingFallbacks += listOf("release") + buildConfigFieldStr( + name = "PUSHER_APP_ID", + value = BuildTimeConfig.PUSHER_APP_ID_NIGHTLY ?: defaultPusherAppId, + ) + } + } + testOptions { unitTests { isIncludeAndroidResources = true diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt index 35482aa164..7aa519a791 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt @@ -10,7 +10,6 @@ package io.element.android.libraries.push.impl import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding -import io.element.android.appconfig.PushConfig import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta @@ -74,7 +73,7 @@ class DefaultPusherSubscriber( ): SetHttpPusherData = SetHttpPusherData( pushKey = pushKey, - appId = PushConfig.PUSHER_APP_ID, + appId = BuildConfig.PUSHER_APP_ID, // TODO + abs(activeSessionHolder.getActiveSession().myUserId.hashCode()) profileTag = DEFAULT_PUSHER_FILE_TAG + "_", // TODO localeProvider.current().language @@ -104,7 +103,7 @@ class DefaultPusherSubscriber( .unsetHttpPusher( unsetHttpPusherData = UnsetHttpPusherData( pushKey = pushKey, - appId = PushConfig.PUSHER_APP_ID + appId = BuildConfig.PUSHER_APP_ID ) ) .onSuccess { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt index e9af494686..d2f421775c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenter.kt @@ -18,7 +18,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.compose.LifecycleResumeEffect import dev.zacsweers.metro.Inject import io.element.android.libraries.architecture.Presenter -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.impl.push.MutableBatteryOptimizationStore import io.element.android.libraries.push.impl.store.PushDataStore @@ -48,12 +48,12 @@ class BatteryOptimizationPresenter( onPauseOrDispose {} } - fun handleEvent(event: BatteryOptimizationEvents) { + fun handleEvent(event: BatteryOptimizationEvent) { when (event) { - BatteryOptimizationEvents.Dismiss -> coroutineScope.launch { + BatteryOptimizationEvent.Dismiss -> coroutineScope.launch { mutableBatteryOptimizationStore.onOptimizationBannerDismissed() } - BatteryOptimizationEvents.RequestDisableOptimizations -> { + BatteryOptimizationEvent.RequestDisableOptimizations -> { isRequestSent = true if (batteryOptimization.requestDisablingBatteryOptimization().not()) { // If not able to perform the request, ensure that we do not display the banner again diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushModule.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushBindingContainer.kt similarity index 88% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushModule.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushBindingContainer.kt index b7e807523c..2274232406 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushModule.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/di/PushBindingContainer.kt @@ -22,10 +22,10 @@ import io.element.android.libraries.push.impl.battery.BatteryOptimizationPresent @BindingContainer @ContributesTo(AppScope::class) -interface PushModule { +interface PushBindingContainer { companion object { @Provides - fun provideNotificationCompatManager(@ApplicationContext context: Context): NotificationManagerCompat { + fun providesNotificationManagerCompat(@ApplicationContext context: Context): NotificationManagerCompat { return NotificationManagerCompat.from(context) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/di/PushHistoryModule.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/di/PushHistoryBindingContainer.kt similarity index 96% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/di/PushHistoryModule.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/di/PushHistoryBindingContainer.kt index 93194b2738..f50b221c58 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/di/PushHistoryModule.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/history/di/PushHistoryBindingContainer.kt @@ -23,10 +23,10 @@ import timber.log.Timber @BindingContainer @ContributesTo(AppScope::class) -object PushHistoryModule { +object PushHistoryBindingContainer { @Provides @SingleIn(AppScope::class) - fun providePushDatabase( + fun providesPushDatabase( @ApplicationContext context: Context, ): PushDatabase { val name = "push_database" diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/test/TestPush.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/test/TestPush.kt index 4ed8ca8338..6921c050db 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/test/TestPush.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/test/TestPush.kt @@ -10,9 +10,9 @@ package io.element.android.libraries.push.impl.test import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding -import io.element.android.appconfig.PushConfig import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.push.impl.BuildConfig import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest import io.element.android.libraries.pushproviders.api.Config @@ -28,7 +28,7 @@ class DefaultTestPush( pushGatewayNotifyRequest.execute( PushGatewayNotifyRequest.Params( url = config.url, - appId = PushConfig.PUSHER_APP_ID, + appId = BuildConfig.PUSHER_APP_ID, pushKey = config.pushKey, eventId = TEST_EVENT_ID, roomId = TEST_ROOM_ID, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriberTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriberTest.kt index 8cb1e2a7f6..76479720be 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriberTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriberTest.kt @@ -9,7 +9,6 @@ package io.element.android.libraries.push.impl import com.google.common.truth.Truth.assertThat -import io.element.android.appconfig.PushConfig import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData @@ -96,7 +95,7 @@ class DefaultPusherSubscriberTest { value( SetHttpPusherData( pushKey = "aPushKey", - appId = PushConfig.PUSHER_APP_ID, + appId = BuildConfig.PUSHER_APP_ID, url = "aGateway", appDisplayName = "MyApp", deviceDisplayName = "MyDevice", @@ -163,7 +162,7 @@ class DefaultPusherSubscriberTest { value( UnsetHttpPusherData( pushKey = "aPushKey", - appId = PushConfig.PUSHER_APP_ID, + appId = BuildConfig.PUSHER_APP_ID, ), ) ) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt index d3b2c8da2b..65bc3cf155 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/battery/BatteryOptimizationPresenterTest.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.push.impl.battery import androidx.lifecycle.Lifecycle import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.push.api.battery.BatteryOptimizationEvents +import io.element.android.libraries.push.api.battery.BatteryOptimizationEvent import io.element.android.libraries.push.impl.push.FakeMutableBatteryOptimizationStore import io.element.android.libraries.push.impl.push.MutableBatteryOptimizationStore import io.element.android.libraries.push.impl.store.InMemoryPushDataStore @@ -98,7 +98,7 @@ class BatteryOptimizationPresenterTest { assertThat(initialState.shouldDisplayBanner).isFalse() val displayedItem = awaitItem() assertThat(displayedItem.shouldDisplayBanner).isTrue() - displayedItem.eventSink(BatteryOptimizationEvents.Dismiss) + displayedItem.eventSink(BatteryOptimizationEvent.Dismiss) onOptimizationBannerDismissedResult.assertions().isCalledOnce() } } @@ -124,7 +124,7 @@ class BatteryOptimizationPresenterTest { assertThat(initialState.shouldDisplayBanner).isFalse() val displayedItem = awaitItem() assertThat(displayedItem.shouldDisplayBanner).isTrue() - displayedItem.eventSink(BatteryOptimizationEvents.RequestDisableOptimizations) + displayedItem.eventSink(BatteryOptimizationEvent.RequestDisableOptimizations) requestDisablingBatteryOptimizationResult.assertions().isCalledOnce() onOptimizationBannerDismissedResult.assertions().isCalledOnce() } @@ -150,7 +150,7 @@ class BatteryOptimizationPresenterTest { assertThat(initialState.shouldDisplayBanner).isFalse() val displayedItem = awaitItem() assertThat(displayedItem.shouldDisplayBanner).isTrue() - displayedItem.eventSink(BatteryOptimizationEvents.RequestDisableOptimizations) + displayedItem.eventSink(BatteryOptimizationEvent.RequestDisableOptimizations) requestDisablingBatteryOptimizationResult.assertions().isCalledOnce() batteryOptimization.isIgnoringBatteryOptimizationsResult = true lifeCycleOwner.givenState(Lifecycle.State.RESUMED) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/test/DefaultTestPushTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/test/DefaultTestPushTest.kt index 9ab0f16b62..3722c09d99 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/test/DefaultTestPushTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/test/DefaultTestPushTest.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.push.impl.test -import io.element.android.appconfig.PushConfig +import io.element.android.libraries.push.impl.BuildConfig import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest import io.element.android.libraries.pushproviders.test.aSessionPushConfig import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -33,7 +33,7 @@ class DefaultTestPushTest { value( PushGatewayNotifyRequest.Params( url = aConfig.url, - appId = PushConfig.PUSHER_APP_ID, + appId = BuildConfig.PUSHER_APP_ID, pushKey = aConfig.pushKey, eventId = DefaultTestPush.TEST_EVENT_ID, roomId = DefaultTestPush.TEST_ROOM_ID, diff --git a/libraries/pushproviders/firebase/README.md b/libraries/pushproviders/firebase/README.md index a2a7ad97ed..d9732af0d0 100644 --- a/libraries/pushproviders/firebase/README.md +++ b/libraries/pushproviders/firebase/README.md @@ -2,6 +2,6 @@ ## Configuration -In order to make this module only know about Firebase, the plugin `com.google.gms.google-services` has been disabled from the `app` module. +In order to make only this module know about Firebase, the plugin `com.google.gms.google-services` has been disabled from the `app` module. -To be able to change the values set to `google_app_id` in the file `build.gradle.kts` of this module, you should enable the plugin `com.google.gms.google-services` again, copy the file `google-services.json` to the folder `/app/src/main`, build the project, and check the generated file `app/build/generated/res/google-services//values/values.xml` to import the generated values into the `build.gradle.kts` files. +To be able to change the values set to `google_app_id` in the file `build.gradle.kts` of this module, you should enable the plugin `com.google.gms.google-services` again, copy the file `google-services.json` to the folder `/app`, build the project, and check the generated file `app/build/generated/res/google-services//values/values.xml` to import the generated values into the `firebase.xml` files. diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts index 1a8e73a4da..33e88dd94c 100644 --- a/libraries/pushproviders/firebase/build.gradle.kts +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -8,7 +8,6 @@ @file:Suppress("UnstableApiUsage") -import config.BuildTimeConfig import extension.setupDependencyInjection import extension.testCommonDependencies import org.gradle.kotlin.dsl.withType @@ -28,27 +27,10 @@ android { buildTypes { getByName("release") { consumerProguardFiles("consumer-proguard-rules.pro") - resValue( - type = "string", - name = "google_app_id", - value = BuildTimeConfig.GOOGLE_APP_ID_RELEASE, - ) - } - getByName("debug") { - resValue( - type = "string", - name = "google_app_id", - value = BuildTimeConfig.GOOGLE_APP_ID_DEBUG, - ) } register("nightly") { consumerProguardFiles("consumer-proguard-rules.pro") matchingFallbacks += listOf("release") - resValue( - type = "string", - name = "google_app_id", - value = BuildTimeConfig.GOOGLE_APP_ID_NIGHTLY, - ) } } } diff --git a/libraries/pushproviders/firebase/src/debug/res/values/firebase.xml b/libraries/pushproviders/firebase/src/debug/res/values/firebase.xml new file mode 100644 index 0000000000..e96f163603 --- /dev/null +++ b/libraries/pushproviders/firebase/src/debug/res/values/firebase.xml @@ -0,0 +1,4 @@ + + + 1:912726360885:android:def0a4e454042e9b00427c + diff --git a/libraries/pushproviders/firebase/src/nightly/res/values/firebase.xml b/libraries/pushproviders/firebase/src/nightly/res/values/firebase.xml new file mode 100644 index 0000000000..7275ff2ddd --- /dev/null +++ b/libraries/pushproviders/firebase/src/nightly/res/values/firebase.xml @@ -0,0 +1,4 @@ + + + 1:912726360885:android:e17435e0beb0303000427c + diff --git a/libraries/pushproviders/firebase/src/release/res/values/firebase.xml b/libraries/pushproviders/firebase/src/release/res/values/firebase.xml new file mode 100644 index 0000000000..3a9a4330d1 --- /dev/null +++ b/libraries/pushproviders/firebase/src/release/res/values/firebase.xml @@ -0,0 +1,4 @@ + + + 1:912726360885:android:d097de99a4c23d2700427c + diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStatePreviewParam.kt similarity index 97% rename from libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt rename to libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStatePreviewParam.kt index c7fa907be2..41fbf93532 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStateProvider.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectStatePreviewParam.kt @@ -20,7 +20,7 @@ import io.element.android.libraries.roomselect.api.RoomSelectMode import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -open class RoomSelectStateProvider : PreviewParameterProvider { +open class RoomSelectStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aRoomSelectState(), diff --git a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt index 357d86dd5a..43b91068f9 100644 --- a/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt +++ b/libraries/roomselect/impl/src/main/kotlin/io/element/android/libraries/roomselect/impl/RoomSelectView.kt @@ -296,7 +296,7 @@ private fun RoomSummaryView( @PreviewsDayNight @Composable -internal fun RoomSelectViewPreview(@PreviewParameter(RoomSelectStateProvider::class) state: RoomSelectState) = ElementPreview { +internal fun RoomSelectViewPreview(@PreviewParameter(RoomSelectStatePreviewParam::class) state: RoomSelectState) = ElementPreview { RoomSelectView( state = state, onDismiss = {}, diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageBindingContainer.kt similarity index 96% rename from libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt rename to libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageBindingContainer.kt index 8955aa4a27..e64268d608 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageModule.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/di/SessionStorageBindingContainer.kt @@ -23,10 +23,10 @@ import timber.log.Timber @BindingContainer @ContributesTo(AppScope::class) -object SessionStorageModule { +object SessionStorageBindingContainer { @Provides @SingleIn(AppScope::class) - fun provideMatrixDatabase( + fun providesSessionDatabase( @ApplicationContext context: Context, ): SessionDatabase { val name = "session_database" diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt index 9b2260d441..e4a1dbf5a1 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/ComposerModeView.kt @@ -192,7 +192,7 @@ private fun ReplyToModeView( @PreviewsDayNight @Composable internal fun ComposerModeViewPreview( - @PreviewParameter(MessageComposerModeSpecialProvider::class) mode: MessageComposerMode.Special + @PreviewParameter(MessageComposerModeSpecialPreviewParam::class) mode: MessageComposerMode.Special ) = ElementPreview { ComposerModeView( composerMode = mode, diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerModeSpecialProvider.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerModeSpecialPreviewParam.kt similarity index 74% rename from libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerModeSpecialProvider.kt rename to libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerModeSpecialPreviewParam.kt index 60850d26c2..2fd367190e 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerModeSpecialProvider.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/MessageComposerModeSpecialPreviewParam.kt @@ -9,15 +9,15 @@ package io.element.android.libraries.textcomposer import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsProvider +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsPreviewParam import io.element.android.libraries.textcomposer.model.MessageComposerMode -class MessageComposerModeSpecialProvider : PreviewParameterProvider { +class MessageComposerModeSpecialPreviewParam : PreviewParameterProvider { override val values: Sequence = sequenceOf( aMessageComposerModeEdit() ) + - // Keep only 3 values from InReplyToDetailsProvider - InReplyToDetailsProvider().values.take(3).map { + // Keep only 3 values from InReplyToDetailsPreviewParam + InReplyToDetailsPreviewParam().values.take(3).map { aMessageComposerModeReply( replyToDetails = it ) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 66c9dcba03..fcb89247b5 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -76,7 +76,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MessageConten import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails -import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsProvider +import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsPreviewParam import io.element.android.libraries.matrix.ui.messages.reply.aProfileDetailsReady import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag @@ -245,6 +245,20 @@ fun TextComposer( canSendTextMessage, ) { when { + composerMode.isEditing -> EndButtonParams( + endButtonContentDescriptionResId = CommonStrings.action_send_edited_message, + endButtonClick = { + if (canSendTextMessage) { + onSendMessage() + } + }, + endButtonContent = @Composable { + SendButtonIcon( + canSendMessage = canSendTextMessage, + isEditing = true, + ) + }, + ) !canSendTextMessage -> when (voiceMessageState) { VoiceMessageState.Idle -> EndButtonParams( @@ -296,18 +310,6 @@ fun TextComposer( ) } } - composerMode.isEditing -> EndButtonParams( - endButtonContentDescriptionResId = CommonStrings.action_send_edited_message, - endButtonClick = { - onSendMessage() - }, - endButtonContent = @Composable { - SendButtonIcon( - canSendMessage = true, - isEditing = true, - ) - }, - ) else -> EndButtonParams( endButtonContentDescriptionResId = CommonStrings.action_send_message, endButtonClick = { @@ -859,7 +861,7 @@ internal fun MarkdownTextComposerEditPreview() = ElementPreview { @PreviewsDayNight @Composable -internal fun TextComposerReplyPreview(@PreviewParameter(InReplyToDetailsProvider::class) inReplyToDetails: InReplyToDetails) = ElementPreview { +internal fun TextComposerReplyPreview(@PreviewParameter(InReplyToDetailsPreviewParam::class) inReplyToDetails: InReplyToDetails) = ElementPreview { PreviewColumn( items = aTextEditorStateRichList() ) { textEditorState -> @@ -883,7 +885,7 @@ internal fun TextComposerReplyPreview(@PreviewParameter(InReplyToDetailsProvider heightDp = 800, ) @Composable -internal fun TextComposerReplyNotEncryptedPreview(@PreviewParameter(InReplyToDetailsProvider::class) inReplyToDetails: InReplyToDetails) = ElementPreview { +internal fun TextComposerReplyNotEncryptedPreview(@PreviewParameter(InReplyToDetailsPreviewParam::class) inReplyToDetails: InReplyToDetails) = ElementPreview { PreviewColumn( items = aTextEditorStateRichList(isRoomEncrypted = false) ) { textEditorState -> diff --git a/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/TextComposerEndButtonTest.kt b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/TextComposerEndButtonTest.kt new file mode 100644 index 0000000000..c979e8c909 --- /dev/null +++ b/libraries/textcomposer/impl/src/test/kotlin/io/element/android/libraries/textcomposer/TextComposerEndButtonTest.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * Copyright 2026 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +@file:OptIn(ExperimentalTestApi::class) + +package io.element.android.libraries.textcomposer + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import io.element.android.libraries.textcomposer.model.MessageComposerMode +import io.element.android.libraries.textcomposer.model.VoiceMessageState +import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.robolectric.RobolectricTest +import io.element.android.wysiwyg.display.TextDisplay +import org.junit.Test + +class TextComposerEndButtonTest : RobolectricTest() { + @Test + fun `editing a message with text shows the send edited message button`() = runAndroidComposeUiTest { + setTextComposer(text = "Some text", composerMode = aMessageComposerModeEdit()) + onNodeWithContentDescription(getString(CommonStrings.action_send_edited_message)).assertIsDisplayed() + } + + @Test + fun `emptying the composer while editing keeps the send edited message button`() = runAndroidComposeUiTest { + setTextComposer(text = "", composerMode = aMessageComposerModeEdit()) + onNodeWithContentDescription(getString(CommonStrings.action_send_edited_message)).assertIsDisplayed() + onNodeWithContentDescription(getString(CommonStrings.a11y_voice_message_record)).assertDoesNotExist() + } + + @Test + fun `emptying the composer while editing a caption keeps the send edited message button`() = runAndroidComposeUiTest { + setTextComposer(text = "", composerMode = aMessageComposerModeEditCaption(content = "An existing caption")) + onNodeWithContentDescription(getString(CommonStrings.action_send_edited_message)).assertIsDisplayed() + onNodeWithContentDescription(getString(CommonStrings.a11y_voice_message_record)).assertDoesNotExist() + } + + @Test + fun `an empty composer outside of edition still offers to record a voice message`() = runAndroidComposeUiTest { + setTextComposer(text = "", composerMode = MessageComposerMode.Normal) + onNodeWithContentDescription(getString(CommonStrings.a11y_voice_message_record)).assertIsDisplayed() + } + + private fun AndroidComposeUiTest.getString(resId: Int): String { + return activity!!.getString(resId) + } + + private fun AndroidComposeUiTest.setTextComposer( + text: String, + composerMode: MessageComposerMode, + ) { + setContent { + TextComposer( + state = aTextEditorStateMarkdown(initialText = text, initialFocus = true), + voiceMessageState = VoiceMessageState.Idle, + composerMode = composerMode, + onRequestFocus = {}, + onSendMessage = {}, + onResetComposerMode = {}, + onAddAttachment = {}, + onDismissTextFormatting = {}, + onVoiceRecorderEvent = {}, + onVoicePlayerEvent = {}, + onSendVoiceMessage = {}, + onDeleteVoiceMessage = {}, + onError = {}, + onTyping = {}, + onReceiveSuggestion = {}, + onSelectRichContent = null, + resolveMentionDisplay = { _, _ -> TextDisplay.Plain }, + resolveAtRoomMentionDisplay = { TextDisplay.Plain }, + ) + } + } +} diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsEvents.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsEvent.kt similarity index 76% rename from libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsEvents.kt rename to libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsEvent.kt index 94330907ea..a8ac210dd8 100644 --- a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsEvents.kt +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsEvent.kt @@ -8,8 +8,8 @@ package io.element.android.libraries.troubleshoot.impl -sealed interface TroubleshootNotificationsEvents { - data object StartTests : TroubleshootNotificationsEvents - data object RetryFailedTests : TroubleshootNotificationsEvents - data class QuickFix(val testIndex: Int) : TroubleshootNotificationsEvents +sealed interface TroubleshootNotificationsEvent { + data object StartTests : TroubleshootNotificationsEvent + data object RetryFailedTests : TroubleshootNotificationsEvent + data class QuickFix(val testIndex: Int) : TroubleshootNotificationsEvent } diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenter.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenter.kt index 00958b1f99..a2c3cc9a58 100644 --- a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenter.kt +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenter.kt @@ -38,19 +38,19 @@ class TroubleshootNotificationsPresenter( } val testSuiteState by troubleshootTestSuite.state.collectAsState() - fun handleEvent(event: TroubleshootNotificationsEvents) { + fun handleEvent(event: TroubleshootNotificationsEvent) { when (event) { - TroubleshootNotificationsEvents.StartTests -> coroutineScope.launch { + TroubleshootNotificationsEvent.StartTests -> coroutineScope.launch { troubleshootTestSuite.runTestSuite(this) } - is TroubleshootNotificationsEvents.QuickFix -> coroutineScope.launch { + is TroubleshootNotificationsEvent.QuickFix -> coroutineScope.launch { troubleshootTestSuite.quickFix( testIndex = event.testIndex, coroutineScope = this, navigator = navigator, ) } - TroubleshootNotificationsEvents.RetryFailedTests -> coroutineScope.launch { + TroubleshootNotificationsEvent.RetryFailedTests -> coroutineScope.launch { troubleshootTestSuite.retryFailedTest(this) } } diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsState.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsState.kt index 92f4d668c4..b6ce940a9f 100644 --- a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsState.kt +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsState.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.troubleshoot.impl data class TroubleshootNotificationsState( val testSuiteState: TroubleshootTestSuiteState, - val eventSink: (TroubleshootNotificationsEvents) -> Unit, + val eventSink: (TroubleshootNotificationsEvent) -> Unit, ) { val hasFailedTests: Boolean = testSuiteState.mainState.isFailure() } diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsStateProvider.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsStatePreviewParam.kt similarity index 96% rename from libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsStateProvider.kt rename to libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsStatePreviewParam.kt index 5cc565559e..1f1aef6767 100644 --- a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsStateProvider.kt +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsStatePreviewParam.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState import kotlinx.collections.immutable.toImmutableList -open class TroubleshootNotificationsStateProvider : PreviewParameterProvider { +open class TroubleshootNotificationsStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aTroubleshootNotificationsState( @@ -78,7 +78,7 @@ open class TroubleshootNotificationsStateProvider : PreviewParameterProvider = emptyList(), - eventSink: (TroubleshootNotificationsEvents) -> Unit = {}, + eventSink: (TroubleshootNotificationsEvent) -> Unit = {}, ) = TroubleshootNotificationsState( eventSink = eventSink, testSuiteState = TroubleshootTestSuiteState( diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsView.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsView.kt index 4e0f89155d..f6b085bc14 100644 --- a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsView.kt +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsView.kt @@ -44,7 +44,7 @@ fun TroubleshootNotificationsView( when (event) { Lifecycle.Event.ON_RESUME -> { if (state.hasFailedTests) { - state.eventSink(TroubleshootNotificationsEvents.RetryFailedTests) + state.eventSink(TroubleshootNotificationsEvent.RetryFailedTests) } } else -> Unit @@ -129,7 +129,7 @@ private fun ColumnScope.TroubleshootNotificationsContent(state: TroubleshootNoti TestSuiteView( testSuiteState = state.testSuiteState, onQuickFixClick = { - state.eventSink(TroubleshootNotificationsEvents.QuickFix(it)) + state.eventSink(TroubleshootNotificationsEvent.QuickFix(it)) } ) } @@ -181,7 +181,7 @@ private fun RunTestButton(state: TroubleshootNotificationsState) { } ), onClick = { - state.eventSink(TroubleshootNotificationsEvents.StartTests) + state.eventSink(TroubleshootNotificationsEvent.StartTests) }, modifier = Modifier.fillMaxWidth(), ) @@ -207,7 +207,7 @@ private fun ColumnScope.TestSuiteView( @PreviewsDayNight @Composable internal fun TroubleshootNotificationsViewPreview( - @PreviewParameter(TroubleshootNotificationsStateProvider::class) state: TroubleshootNotificationsState, + @PreviewParameter(TroubleshootNotificationsStatePreviewParam::class) state: TroubleshootNotificationsState, ) = ElementPreview { TroubleshootNotificationsView( state = state, diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryEvents.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryEvent.kt similarity index 80% rename from libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryEvents.kt rename to libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryEvent.kt index 5116e0e101..a73c466275 100644 --- a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryEvents.kt +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryEvent.kt @@ -12,9 +12,9 @@ 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.SessionId -sealed interface PushHistoryEvents { - data class SetShowOnlyErrors(val showOnlyErrors: Boolean) : PushHistoryEvents - data class Reset(val requiresConfirmation: Boolean) : PushHistoryEvents - data class NavigateTo(val sessionId: SessionId, val roomId: RoomId, val eventId: EventId) : PushHistoryEvents - data object ClearDialog : PushHistoryEvents +sealed interface PushHistoryEvent { + data class SetShowOnlyErrors(val showOnlyErrors: Boolean) : PushHistoryEvent + data class Reset(val requiresConfirmation: Boolean) : PushHistoryEvent + data class NavigateTo(val sessionId: SessionId, val roomId: RoomId, val eventId: EventId) : PushHistoryEvent + data object ClearDialog : PushHistoryEvent } diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryPresenter.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryPresenter.kt index ad1878051d..94e220a726 100644 --- a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryPresenter.kt +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryPresenter.kt @@ -62,12 +62,12 @@ class PushHistoryPresenter( var resetAction: AsyncAction by remember { mutableStateOf(AsyncAction.Uninitialized) } var showNotSameAccountError by remember { mutableStateOf(false) } - fun handleEvent(event: PushHistoryEvents) { + fun handleEvent(event: PushHistoryEvent) { when (event) { - is PushHistoryEvents.SetShowOnlyErrors -> { + is PushHistoryEvent.SetShowOnlyErrors -> { showOnlyErrors = event.showOnlyErrors } - is PushHistoryEvents.Reset -> { + is PushHistoryEvent.Reset -> { if (event.requiresConfirmation) { resetAction = AsyncAction.ConfirmingNoParams } else { @@ -78,11 +78,11 @@ class PushHistoryPresenter( } } } - PushHistoryEvents.ClearDialog -> { + PushHistoryEvent.ClearDialog -> { resetAction = AsyncAction.Uninitialized showNotSameAccountError = false } - is PushHistoryEvents.NavigateTo -> { + is PushHistoryEvent.NavigateTo -> { if (event.sessionId != sessionId) { showNotSameAccountError = true } else { diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryState.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryState.kt index dbc98bdbe3..7afa567aa1 100644 --- a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryState.kt +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryState.kt @@ -18,5 +18,5 @@ data class PushHistoryState( val showOnlyErrors: Boolean, val resetAction: AsyncAction, val showNotSameAccountError: Boolean, - val eventSink: (PushHistoryEvents) -> Unit, + val eventSink: (PushHistoryEvent) -> Unit, ) diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryStateProvider.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryStatePreviewParam.kt similarity index 95% rename from libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryStateProvider.kt rename to libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryStatePreviewParam.kt index e7384048f4..cec0069237 100644 --- a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryStateProvider.kt +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryStatePreviewParam.kt @@ -16,7 +16,7 @@ import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.push.api.history.PushHistoryItem import kotlinx.collections.immutable.toImmutableList -open class PushHistoryStateProvider : PreviewParameterProvider { +open class PushHistoryStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aPushHistoryState(), @@ -53,7 +53,7 @@ fun aPushHistoryState( showOnlyErrors: Boolean = false, resetAction: AsyncAction = AsyncAction.Uninitialized, showNotSameAccountError: Boolean = false, - eventSink: (PushHistoryEvents) -> Unit = {}, + eventSink: (PushHistoryEvent) -> Unit = {}, ) = PushHistoryState( pushCounter = pushCounter, pushHistoryItems = pushHistoryItems.toImmutableList(), diff --git a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryView.kt b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryView.kt index 21e9c64139..be10a2845e 100644 --- a/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryView.kt +++ b/libraries/troubleshoot/impl/src/main/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryView.kt @@ -100,14 +100,14 @@ fun PushHistoryView( }, onClick = { showMenu = false - state.eventSink(PushHistoryEvents.SetShowOnlyErrors(state.showOnlyErrors.not())) + state.eventSink(PushHistoryEvent.SetShowOnlyErrors(state.showOnlyErrors.not())) }, ) DropdownMenuItem( text = { Text(stringResource(id = CommonStrings.action_reset)) }, onClick = { showMenu = false - state.eventSink(PushHistoryEvents.Reset(requiresConfirmation = true)) + state.eventSink(PushHistoryEvent.Reset(requiresConfirmation = true)) }, ) } @@ -132,8 +132,8 @@ fun PushHistoryView( title = stringResource(CommonStrings.dialog_title_confirmation), submitText = stringResource(CommonStrings.action_reset), cancelText = stringResource(CommonStrings.action_cancel), - onSubmitClick = { state.eventSink(PushHistoryEvents.Reset(requiresConfirmation = false)) }, - onDismiss = { state.eventSink(PushHistoryEvents.ClearDialog) }, + onSubmitClick = { state.eventSink(PushHistoryEvent.Reset(requiresConfirmation = false)) }, + onDismiss = { state.eventSink(PushHistoryEvent.ClearDialog) }, ) }, onErrorDismiss = {}, @@ -142,7 +142,7 @@ fun PushHistoryView( if (state.showNotSameAccountError) { ErrorDialog( content = "Please switch account first to navigate to the event.", - onSubmit = { state.eventSink(PushHistoryEvents.ClearDialog) } + onSubmit = { state.eventSink(PushHistoryEvent.ClearDialog) } ) } } @@ -175,7 +175,7 @@ private fun PushHistoryContent( val roomId = pushHistory.roomId val eventId = pushHistory.eventId if (sessionId != null && roomId != null && eventId != null) { - state.eventSink(PushHistoryEvents.NavigateTo(sessionId, roomId, eventId)) + state.eventSink(PushHistoryEvent.NavigateTo(sessionId, roomId, eventId)) } } ) @@ -268,7 +268,7 @@ private fun PushHistoryItem( @PreviewsDayNight @Composable internal fun PushHistoryViewPreview( - @PreviewParameter(PushHistoryStateProvider::class) state: PushHistoryState, + @PreviewParameter(PushHistoryStatePreviewParam::class) state: PushHistoryState, ) = ElementPreview { PushHistoryView( state = state, diff --git a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenterTest.kt b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenterTest.kt index c8f8382ecf..32ab07883b 100644 --- a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenterTest.kt +++ b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsPresenterTest.kt @@ -42,7 +42,7 @@ class TroubleshootNotificationsPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink(TroubleshootNotificationsEvents.StartTests) + initialState.eventSink(TroubleshootNotificationsEvent.StartTests) skipItems(1) val stateAfterStart = awaitItem() assertThat(stateAfterStart.testSuiteState.mainState).isEqualTo(AsyncAction.Loading) @@ -63,7 +63,7 @@ class TroubleshootNotificationsPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink(TroubleshootNotificationsEvents.RetryFailedTests) + initialState.eventSink(TroubleshootNotificationsEvent.RetryFailedTests) skipItems(1) val stateAfterStart = awaitItem() assertThat(stateAfterStart.testSuiteState.mainState).isEqualTo(AsyncAction.Loading) @@ -160,7 +160,7 @@ class TroubleshootNotificationsPresenterTest { skipItems(1) val initialState = awaitItem() assertThat(initialState.testSuiteState.mainState).isInstanceOf(AsyncAction.Failure::class.java) - initialState.eventSink(TroubleshootNotificationsEvents.QuickFix(0)) + initialState.eventSink(TroubleshootNotificationsEvent.QuickFix(0)) val stateAfterStart = awaitItem() assertThat(stateAfterStart.testSuiteState.mainState).isEqualTo(AsyncAction.Loading) } diff --git a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsViewTest.kt b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsViewTest.kt index ea98aeabc6..b685eb2238 100644 --- a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsViewTest.kt +++ b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/TroubleshootNotificationsViewTest.kt @@ -27,7 +27,7 @@ import org.robolectric.annotation.Config class TroubleshootNotificationsViewTest : RobolectricTest() { @Test fun `press menu back invokes the expected callback`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) ensureCalledOnce { setTroubleshootNotificationsView( state = aTroubleshootNotificationsState( @@ -41,20 +41,20 @@ class TroubleshootNotificationsViewTest : RobolectricTest() { @Test fun `clicking on run test emits the expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setTroubleshootNotificationsView( aTroubleshootNotificationsState( eventSink = eventsRecorder ), ) onNodeWithText("Run tests").performClick() - eventsRecorder.assertSingle(TroubleshootNotificationsEvents.StartTests) + eventsRecorder.assertSingle(TroubleshootNotificationsEvent.StartTests) } @Config(qualifiers = "h1024dp") @Test fun `clicking on run test again emits the expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setTroubleshootNotificationsView( aTroubleshootNotificationsState( tests = listOf( @@ -68,8 +68,8 @@ class TroubleshootNotificationsViewTest : RobolectricTest() { onNodeWithText("Run tests again").performClick() eventsRecorder.assertList( listOf( - TroubleshootNotificationsEvents.RetryFailedTests, - TroubleshootNotificationsEvents.StartTests, + TroubleshootNotificationsEvent.RetryFailedTests, + TroubleshootNotificationsEvent.StartTests, ) ) } @@ -77,7 +77,7 @@ class TroubleshootNotificationsViewTest : RobolectricTest() { @Config(qualifiers = "h1024dp") @Test fun `clicking on quick fix emits the expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setTroubleshootNotificationsView( aTroubleshootNotificationsState( tests = listOf( @@ -91,8 +91,8 @@ class TroubleshootNotificationsViewTest : RobolectricTest() { onNodeWithText("Attempt to fix").performClick() eventsRecorder.assertList( listOf( - TroubleshootNotificationsEvents.RetryFailedTests, - TroubleshootNotificationsEvents.QuickFix(0), + TroubleshootNotificationsEvent.RetryFailedTests, + TroubleshootNotificationsEvent.QuickFix(0), ) ) } diff --git a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryPresenterTest.kt b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryPresenterTest.kt index 15fb6b5f25..e378cc9af6 100644 --- a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryPresenterTest.kt +++ b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryPresenterTest.kt @@ -73,9 +73,9 @@ class PushHistoryPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink(PushHistoryEvents.Reset(requiresConfirmation = true)) + initialState.eventSink(PushHistoryEvent.Reset(requiresConfirmation = true)) assertThat(awaitItem().resetAction).isEqualTo(AsyncAction.ConfirmingNoParams) - initialState.eventSink(PushHistoryEvents.ClearDialog) + initialState.eventSink(PushHistoryEvent.ClearDialog) assertThat(awaitItem().resetAction).isEqualTo(AsyncAction.Uninitialized) resetPushHistoryResult.assertions().isNeverCalled() } @@ -92,9 +92,9 @@ class PushHistoryPresenterTest { ) presenter.test { val initialState = awaitItem() - initialState.eventSink(PushHistoryEvents.Reset(requiresConfirmation = true)) + initialState.eventSink(PushHistoryEvent.Reset(requiresConfirmation = true)) assertThat(awaitItem().resetAction).isEqualTo(AsyncAction.ConfirmingNoParams) - initialState.eventSink(PushHistoryEvents.Reset(requiresConfirmation = false)) + initialState.eventSink(PushHistoryEvent.Reset(requiresConfirmation = false)) assertThat(awaitItem().resetAction).isEqualTo(AsyncAction.Loading) assertThat(awaitItem().resetAction).isEqualTo(AsyncAction.Uninitialized) resetPushHistoryResult.assertions().isCalledOnce() @@ -115,13 +115,13 @@ class PushHistoryPresenterTest { pushService.emitPushHistoryItems(listOf(item, itemError)) awaitItem().let { state -> assertThat(state.pushHistoryItems).containsExactly(item, itemError) - state.eventSink(PushHistoryEvents.SetShowOnlyErrors(showOnlyErrors = true)) + state.eventSink(PushHistoryEvent.SetShowOnlyErrors(showOnlyErrors = true)) } skipItems(1) awaitItem().let { state -> assertThat(state.showOnlyErrors).isTrue() assertThat(state.pushHistoryItems).containsExactly(itemError) - state.eventSink(PushHistoryEvents.SetShowOnlyErrors(showOnlyErrors = false)) + state.eventSink(PushHistoryEvent.SetShowOnlyErrors(showOnlyErrors = false)) } skipItems(1) awaitItem().let { state -> @@ -142,7 +142,7 @@ class PushHistoryPresenterTest { presenter.test { val initialState = awaitItem() initialState.eventSink( - PushHistoryEvents.NavigateTo( + PushHistoryEvent.NavigateTo( sessionId = A_SESSION_ID, roomId = A_ROOM_ID, eventId = AN_EVENT_ID, @@ -160,7 +160,7 @@ class PushHistoryPresenterTest { presenter.test { val initialState = awaitItem() initialState.eventSink( - PushHistoryEvents.NavigateTo( + PushHistoryEvent.NavigateTo( sessionId = A_SESSION_ID_2, roomId = A_ROOM_ID, eventId = AN_EVENT_ID, @@ -168,7 +168,7 @@ class PushHistoryPresenterTest { ) assertThat(awaitItem().showNotSameAccountError).isTrue() // Reset error - initialState.eventSink(PushHistoryEvents.ClearDialog) + initialState.eventSink(PushHistoryEvent.ClearDialog) assertThat(awaitItem().showNotSameAccountError).isFalse() } } diff --git a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryViewTest.kt b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryViewTest.kt index b9f615d68e..989f7bdc10 100644 --- a/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryViewTest.kt +++ b/libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryViewTest.kt @@ -30,8 +30,8 @@ import org.junit.Test class PushHistoryViewTest : RobolectricTest() { @Test - fun `clicking on Reset sends a PushHistoryEvents`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + fun `clicking on Reset sends a PushHistoryEvent`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() setPushHistoryView( aPushHistoryState( pushCounter = 123, @@ -41,14 +41,14 @@ class PushHistoryViewTest : RobolectricTest() { val menuContentDescription = activity!!.getString(CommonStrings.a11y_user_menu) onNodeWithContentDescription(menuContentDescription).performClick() clickOn(CommonStrings.action_reset) - eventsRecorder.assertSingle(PushHistoryEvents.Reset(requiresConfirmation = true)) + eventsRecorder.assertSingle(PushHistoryEvent.Reset(requiresConfirmation = true)) // Also check that the push counter is rendered onNodeWithText("123").assertExists() } @Test - fun `clicking on show only errors sends a PushHistoryEvents(true)`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + fun `clicking on show only errors sends a PushHistoryEvent(true)`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() setPushHistoryView( aPushHistoryState( showOnlyErrors = false, @@ -58,12 +58,12 @@ class PushHistoryViewTest : RobolectricTest() { val menuContentDescription = activity!!.getString(CommonStrings.a11y_user_menu) onNodeWithContentDescription(menuContentDescription).performClick() onNodeWithText("Show only errors").performClick() - eventsRecorder.assertSingle(PushHistoryEvents.SetShowOnlyErrors(showOnlyErrors = true)) + eventsRecorder.assertSingle(PushHistoryEvent.SetShowOnlyErrors(showOnlyErrors = true)) } @Test - fun `clicking on show only errors sends a PushHistoryEvents(false)`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + fun `clicking on show only errors sends a PushHistoryEvent(false)`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() setPushHistoryView( aPushHistoryState( showOnlyErrors = true, @@ -73,12 +73,12 @@ class PushHistoryViewTest : RobolectricTest() { val menuContentDescription = activity!!.getString(CommonStrings.a11y_user_menu) onNodeWithContentDescription(menuContentDescription).performClick() onNodeWithText("Show only errors").performClick() - eventsRecorder.assertSingle(PushHistoryEvents.SetShowOnlyErrors(showOnlyErrors = false)) + eventsRecorder.assertSingle(PushHistoryEvent.SetShowOnlyErrors(showOnlyErrors = false)) } @Test fun `clicking on an invalid event has no effect`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder(expectEvents = false) + val eventsRecorder = EventsRecorder(expectEvents = false) setPushHistoryView( aPushHistoryState( pushHistoryItems = listOf( @@ -95,7 +95,7 @@ class PushHistoryViewTest : RobolectricTest() { @Test fun `clicking on a valid event emits the expected Event`() = runAndroidComposeUiTest { - val eventsRecorder = EventsRecorder() + val eventsRecorder = EventsRecorder() setPushHistoryView( aPushHistoryState( pushHistoryItems = listOf( @@ -111,7 +111,7 @@ class PushHistoryViewTest : RobolectricTest() { ) onNodeWithText(A_FORMATTED_DATE).performClick() eventsRecorder.assertSingle( - PushHistoryEvents.NavigateTo( + PushHistoryEvent.NavigateTo( sessionId = A_SESSION_ID, roomId = A_ROOM_ID, eventId = AN_EVENT_ID, diff --git a/libraries/ui-strings/src/main/res/values-es/translations.xml b/libraries/ui-strings/src/main/res/values-es/translations.xml index 223b8aa457..8a28af1e9c 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -516,14 +516,6 @@ Motivo: %1$s." "%1$s Android" "Agitar con fuerza para informar de un error" "Captura de pantalla" - "Iniciar sesión" - "Crear cuenta" - "Si tu cuenta te la proporciona tu empresa o una comunidad, cambia el proveedor de la cuenta (ejemplo: nombredeempresa.com)." - "Crea una cuenta gratuita en matrix.org. Si deseas crear una cuenta con otro proveedor, modifícala según corresponda (por ejemplo: communityname.org)." - "Proveedor de cuentas" - "ejemplo.com" - "Introduce tu proveedor de cuenta" - "Elegir proveedor de cuentas" "%1$s: %2$s" "Opciones" "Eliminar %1$s" diff --git a/libraries/ui-strings/src/main/res/values-et/translations.xml b/libraries/ui-strings/src/main/res/values-et/translations.xml index 14800b9ca3..c24e8652f6 100644 --- a/libraries/ui-strings/src/main/res/values-et/translations.xml +++ b/libraries/ui-strings/src/main/res/values-et/translations.xml @@ -507,14 +507,6 @@ Kas sa oled kindel, et soovid jätkata?" "%1$s Android" "Veast teatamiseks raputa nutiseadet ägedalt" "Ekraanitõmmis" - "Logi sisse" - "Loo kasutajakonto" - "Kui sinu kasutajakonto paikneb äriühingu või kogukonna serveris, siis muuda kontoteenuse pakkujat (näiteks: minufirmanimi.ee)." - "Loo tasuta kasutajakonto matrix.org serverisse. Kui soovid luua kasutajakonto mõne teise teenusepakkuja juures, siis muuda seda vastavalt (näiteks: minukogukond.ee)." - "Teenusepakkuja" - "domeeninäidis.ee" - "Sisesta oma kasutajakonto teenusepakkuja" - "Vali kasutajakonto teenusepakkuja" "%1$s: %2$s" "Valikud" "Kustuta: %1$s" diff --git a/libraries/ui-strings/src/main/res/values-fi/translations.xml b/libraries/ui-strings/src/main/res/values-fi/translations.xml index b1cc4889a8..11ca38edf0 100644 --- a/libraries/ui-strings/src/main/res/values-fi/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fi/translations.xml @@ -516,14 +516,6 @@ Haluatko varmasti jatkaa?" "%1$s Android" "Raivostunut ravistaminen ilmoittaa virheestä" "Näyttökuva" - "Kirjaudu sisään" - "Luo tili" - "Jos tilisi on yrityksesi tai yhteisön tarjoama, vaihda palveluntarjoaja (esimerkki: companyname.com)." - "Luo tili matrix.org -palvelimelle ilmaiseksi. Jos haluat luoda tilin eri palveluntarjoajan kautta, muuta sitä vastaavasti (esimerkiksi: communityname.org)." - "Palveluntarjoaja" - "example.com" - "Anna palveluntarjoajasi" - "Valitse palveluntarjoaja" "%1$s: %2$s" "Vaihtoehdot" "Poista %1$s" diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index 105ced1ad5..6aced99b00 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -516,14 +516,6 @@ Raison : %1$s." "%1$s Android" "Rageshake pour signaler un problème" "Capture d’écran" - "Se connecter" - "Créer un compte" - "Si votre fournisseur de compte est fourni par votre entreprise ou une communauté, veuillez le saisir ici (exemple : monentreprise.com)." - "Créez un compte sur matrix.org gratuitement. Si vous souhaitez créer un compte auprès d’un autre fournisseur, veuillez modifier l’adresse ici (exemple : monassociation.org)." - "Fournisseur de compte" - "exemple.com" - "Saisissez votre fournisseur de compte" - "Choisissez un fournisseur de compte" "%1$s: %2$s" "Options" "Supprimer %1$s" diff --git a/libraries/ui-strings/src/main/res/values-pl/translations.xml b/libraries/ui-strings/src/main/res/values-pl/translations.xml index 1ee4275703..f0ac7785da 100644 --- a/libraries/ui-strings/src/main/res/values-pl/translations.xml +++ b/libraries/ui-strings/src/main/res/values-pl/translations.xml @@ -528,14 +528,6 @@ Czy na pewno chcesz kontynuować?" "%1$s Android" "Wstrząśnij gniewnie, aby zgłosić błąd" "Zrzut ekranu" - "Zaloguj" - "Utwórz konto" - "Jeśli uzyskałeś konto od firmy lub społeczności, wprowadź dostawcę konta (na przykład: nazwafirmy.com)" - "Załóż darmowe konto na matrix.org. Jeśli chcesz założyć konto u innego dostawcy wprowadź własny adres (na przykład: nazwaspołeczności.org)." - "Dostawca konta" - "przykład.com" - "Wprowadź dostawcę swojego konta" - "Wybierz dostawcę konta" "%1$s: %2$s" "Opcje" "Usuń %1$s" diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml index 71b5ab7041..cd18373e6a 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -528,14 +528,6 @@ Sunteți sigur că doriți să continuați?" "%1$s Android" "Rageshake pentru a raporta erori" "Captură de ecran" - "Autentificare" - "Creați un cont" - "Dacă contul este furnizat de compania dumneavoastră sau de o comunitate, schimbați furnizorul contului (exemplu: numecompanie.com)." - "Creați un cont gratuit pe matrix.org. Dacă doriți să creați un cont la un alt furnizor, schimbați-l în consecință (exemplu: numecomunitate.org)." - "Furnizor de cont" - "exemplu.ro" - "Introduceți furnizorul contului dumneavoastră" - "Alegeți un furnizor de cont" "%1$s: %2$s" "Opțiuni" "Ștergeți %1$s" diff --git a/libraries/ui-strings/src/main/res/values-uk/translations.xml b/libraries/ui-strings/src/main/res/values-uk/translations.xml index 8545325603..e00a269473 100644 --- a/libraries/ui-strings/src/main/res/values-uk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-uk/translations.xml @@ -528,14 +528,6 @@ "%1$s Android" "Повідомляти про помилку потрусивши телефон" "Знімок екрана" - "Увійти" - "Створити обліковий запис" - "Якщо ваш обліковий запис надано вашою компанією або спільнотою, змініть постачальника облікового запису (наприклад: companyname.com)." - "Створіть обліковий запис на matrix.org безплатно. Якщо ви хочете створити обліковий запис у іншого постачальника, змініть його відповідно (наприклад: communityname.org)." - "Постачальник облікового запису" - "example.com" - "Вкажіть постачальника вашого облікового запису" - "Оберіть постачальника облікового запису" "%1$s: %2$s" "Варіанти" "Вилучити %1$s" diff --git a/libraries/ui-strings/src/main/res/values-zh/translations.xml b/libraries/ui-strings/src/main/res/values-zh/translations.xml index b363ba9498..03929c0182 100644 --- a/libraries/ui-strings/src/main/res/values-zh/translations.xml +++ b/libraries/ui-strings/src/main/res/values-zh/translations.xml @@ -504,14 +504,6 @@ "%1$s Android" "摇一摇以报告 bug" "屏幕截图" - "登录" - "创建账户" - "如果你的账户由你的公司或社区提供,则应更改账户提供者(例如:companyname.com)。" - "在 matrix.org 创建免费账户。如果你想使用其它服务提供者创建账户,请相应地更改域名(例如:communityname.org)。" - "账户提供者" - "example.com" - "输入你的账户提供者" - "选择账户提供者" "%1$s:%2$s" "选项" "移除 %1$s" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 0d27ff5a9f..92899ba9da 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -516,14 +516,6 @@ Are you sure you want to continue?" "%1$s Android" "Rageshake to report bug" "Screenshot" - "Sign in" - "Create account" - "If your account is provided by your company or a community, then change the account provider (example: companyname.com)." - "Create an account on matrix.org for free. If you want to create an account with a different provider, then change it accordingly (example: communityname.org)." - "Account provider" - "example.com" - "Enter your account provider" - "Choose an account provider" "%1$s: %2$s" "Options" "Remove %1$s" diff --git a/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageStateProvider.kt b/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageStatePreviewParam.kt similarity index 95% rename from libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageStateProvider.kt rename to libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageStatePreviewParam.kt index dd8d222e69..f417947cb6 100644 --- a/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageStateProvider.kt +++ b/libraries/voiceplayer/api/src/main/kotlin/io/element/android/libraries/voiceplayer/api/VoiceMessageStatePreviewParam.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.voiceplayer.api import androidx.compose.ui.tooling.preview.PreviewParameterProvider -open class VoiceMessageStateProvider : PreviewParameterProvider { +open class VoiceMessageStatePreviewParam : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aVoiceMessageState( diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/di/VoiceRecorderModule.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/di/VoiceRecorderBindingContainer.kt similarity index 87% rename from libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/di/VoiceRecorderModule.kt rename to libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/di/VoiceRecorderBindingContainer.kt index ee9f7bfa9d..3dd1b902fa 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/di/VoiceRecorderModule.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/di/VoiceRecorderBindingContainer.kt @@ -22,9 +22,9 @@ import io.element.android.opusencoder.OggOpusEncoder @BindingContainer @ContributesTo(RoomScope::class) -object VoiceRecorderModule { +object VoiceRecorderBindingContainer { @Provides - fun provideAudioConfig(): AudioConfig { + fun providesAudioConfig(): AudioConfig { val sampleRate = SampleRate return AudioConfig( format = AudioFormat.Builder() @@ -40,7 +40,7 @@ object VoiceRecorderModule { } @Provides - public fun provideVoiceFileConfig(): VoiceFileConfig = + public fun providesVoiceFileConfig(): VoiceFileConfig = VoiceFileConfig( cacheSubdir = "voice_recordings", fileExt = "ogg", @@ -48,5 +48,5 @@ object VoiceRecorderModule { ) @Provides - fun provideOggOpusEncoder(): OggOpusEncoder = OggOpusEncoder.create() + fun providesOggOpusEncoder(): OggOpusEncoder = OggOpusEncoder.create() } diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorderTest.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorderTest.kt index 158b9ba481..070b341d76 100644 --- a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorderTest.kt +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorderTest.kt @@ -18,7 +18,7 @@ import io.element.android.libraries.voicerecorder.api.VoiceRecorderState import io.element.android.libraries.voicerecorder.impl.audio.Audio import io.element.android.libraries.voicerecorder.impl.audio.AudioConfig import io.element.android.libraries.voicerecorder.impl.audio.SampleRate -import io.element.android.libraries.voicerecorder.impl.di.VoiceRecorderModule +import io.element.android.libraries.voicerecorder.impl.di.VoiceRecorderBindingContainer import io.element.android.libraries.voicerecorder.test.FakeAudioLevelCalculator import io.element.android.libraries.voicerecorder.test.FakeAudioReaderFactory import io.element.android.libraries.voicerecorder.test.FakeEncoder @@ -151,7 +151,7 @@ class DefaultVoiceRecorderTest { } private fun TestScope.createDefaultVoiceRecorder(): DefaultVoiceRecorder { - val fileConfig = VoiceRecorderModule.provideVoiceFileConfig() + val fileConfig = VoiceRecorderBindingContainer.providesVoiceFileConfig() return DefaultVoiceRecorder( dispatchers = testCoroutineDispatchers(), timeSource = timeSource, diff --git a/libraries/voicerecorder/test/build.gradle.kts b/libraries/voicerecorder/test/build.gradle.kts index c0f74b71e8..f54ef0e8d3 100644 --- a/libraries/voicerecorder/test/build.gradle.kts +++ b/libraries/voicerecorder/test/build.gradle.kts @@ -19,6 +19,5 @@ dependencies { implementation(projects.tests.testutils) implementation(libs.coroutines.test) - implementation(libs.test.truth) implementation(projects.libraries.core) } diff --git a/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt b/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt index ec62227857..b735bb9fa5 100644 --- a/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt +++ b/libraries/voicerecorder/test/src/main/kotlin/io/element/android/libraries/voicerecorder/test/FakeVoiceRecorder.kt @@ -8,13 +8,13 @@ package io.element.android.libraries.voicerecorder.test -import com.google.common.truth.Truth.assertThat import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.voicerecorder.api.VoiceRecorder import io.element.android.libraries.voicerecorder.api.VoiceRecorderState +import io.element.android.tests.testutils.lambda.lambdaError +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.yield import java.io.File import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -23,51 +23,47 @@ import kotlin.time.TestTimeSource class FakeVoiceRecorder( private val timeSource: TestTimeSource = TestTimeSource(), private val recordingDuration: Duration = 0.seconds, - private val levels: List = listOf(0.1f, 0.2f) + private val levels: List = listOf(0.1f, 0.2f), + val waveform: List = A_WAVEFORM, + private val startRecordResult: () -> Unit = { lambdaError() }, + private val stopRecordResult: (Boolean) -> Unit = { lambdaError() }, + private val deleteRecordingResult: () -> Unit = { lambdaError() }, ) : VoiceRecorder { private val _state = MutableStateFlow(VoiceRecorderState.Idle) override val state: StateFlow = _state - private var curRecording: File? = null + private val levelInterval = if (levels.isEmpty()) Duration.ZERO else recordingDuration / levels.size - private var securityException: SecurityException? = null + private var currentRecording: File? = null + private var lastRecordingId = 0 + private var activeRecordingId = 0 - private var startedCount = 0 - private var stoppedCount = 0 - private var deletedCount = 0 - - var waveform: List = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f) override suspend fun startRecord() { - startedCount += 1 + startRecordResult() + if (activeRecordingId != 0) return + val recordingId = ++lastRecordingId + activeRecordingId = recordingId + currentRecording = File("file.ogg") val startedAt = timeSource.markNow() - securityException?.let { throw it } - - if (curRecording != null) { - error("Previous recording was not cleared") - } - curRecording = File("file.ogg") - - timeSource += recordingDuration for (i in 1..levels.size) { + delay(levelInterval) + if (activeRecordingId != recordingId) return + timeSource += levelInterval _state.emit(VoiceRecorderState.Recording(startedAt.elapsedNow(), levels.take(i))) - yield() } } - override suspend fun stopRecord( - cancelled: Boolean - ) { - stoppedCount++ - + override suspend fun stopRecord(cancelled: Boolean) { + stopRecordResult(cancelled) + activeRecordingId = 0 if (cancelled) { deleteRecording() } - _state.emit( - when (curRecording) { + when (val file = currentRecording) { null -> VoiceRecorderState.Idle else -> VoiceRecorderState.Finished( - file = curRecording!!, + file = file, mimeType = MimeTypes.Ogg, duration = recordingDuration, waveform = waveform, @@ -77,25 +73,11 @@ class FakeVoiceRecorder( } override suspend fun deleteRecording() { - deletedCount++ - curRecording = null - - _state.emit( - VoiceRecorderState.Idle - ) - } - - fun assertCalls( - started: Int = 0, - stopped: Int = 0, - deleted: Int = 0, - ) { - assertThat(startedCount).isEqualTo(started) - assertThat(stoppedCount).isEqualTo(stopped) - assertThat(deletedCount).isEqualTo(deleted) - } - - fun givenThrowsSecurityException(exception: SecurityException) { - this.securityException = exception + deleteRecordingResult() + activeRecordingId = 0 + currentRecording = null + _state.emit(VoiceRecorderState.Idle) } } + +private val A_WAVEFORM = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f) diff --git a/libraries/workmanager/impl/src/main/kotlin/io/element/android/libraries/workmanager/impl/WorkManagerModule.kt b/libraries/workmanager/impl/src/main/kotlin/io/element/android/libraries/workmanager/impl/di/WorkManagerBindingContainer.kt similarity index 89% rename from libraries/workmanager/impl/src/main/kotlin/io/element/android/libraries/workmanager/impl/WorkManagerModule.kt rename to libraries/workmanager/impl/src/main/kotlin/io/element/android/libraries/workmanager/impl/di/WorkManagerBindingContainer.kt index 7df9e7f431..65399112ef 100644 --- a/libraries/workmanager/impl/src/main/kotlin/io/element/android/libraries/workmanager/impl/WorkManagerModule.kt +++ b/libraries/workmanager/impl/src/main/kotlin/io/element/android/libraries/workmanager/impl/di/WorkManagerBindingContainer.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.workmanager.impl +package io.element.android.libraries.workmanager.impl.di import android.content.Context import androidx.work.WorkManager @@ -18,7 +18,7 @@ import io.element.android.libraries.di.annotations.ApplicationContext @BindingContainer @ContributesTo(AppScope::class) -object WorkManagerModule { +object WorkManagerBindingContainer { @Provides @SingleIn(AppScope::class) fun providesWorkManager( diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index 1c77bea7cf..89ad4a63fc 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -45,7 +45,7 @@ private const val versionMonth = 8 * Release number in the month. Value must be in [0,99]. * Do not update this value. it is updated by the release script. */ -private const val versionReleaseNumber = 1 +private const val versionReleaseNumber = 2 object Versions { /** diff --git a/plugins/src/main/kotlin/config/BuildTimeConfig.kt b/plugins/src/main/kotlin/config/BuildTimeConfig.kt index 83236f1a5a..4160efeb78 100644 --- a/plugins/src/main/kotlin/config/BuildTimeConfig.kt +++ b/plugins/src/main/kotlin/config/BuildTimeConfig.kt @@ -11,18 +11,14 @@ package config object BuildTimeConfig { const val APPLICATION_ID = "io.element.android.x" const val APPLICATION_NAME = "Element X" - const val GOOGLE_APP_ID_RELEASE = "1:912726360885:android:d097de99a4c23d2700427c" - const val GOOGLE_APP_ID_DEBUG = "1:912726360885:android:def0a4e454042e9b00427c" - const val GOOGLE_APP_ID_NIGHTLY = "1:912726360885:android:e17435e0beb0303000427c" - val METADATA_HOST_REVERSED: String? = null + val OAUTH_CLIENT_URL_PATH: String? = "apps/android" val URL_WEBSITE: String? = null val URL_LOGO: String? = null val URL_COPYRIGHT: String? = null val URL_ACCEPTABLE_USE: String? = null val URL_PRIVACY: String? = null val URL_POLICY: String? = null - val OAUTH_CLIENT_URL_PATH: String? = "apps/android" val SERVICES_MAPTILER_BASE_URL: String? = null val SERVICES_MAPTILER_APIKEY: String? = null val SERVICES_MAPTILER_LIGHT_MAPID: String? = null @@ -33,7 +29,9 @@ object BuildTimeConfig { val SERVICES_SENTRY_DSN_RUST: String? = null val BUG_REPORT_URL: String? = null val BUG_REPORT_APP_NAME: String? = null - - const val PUSH_CONFIG_INCLUDE_FIREBASE = true - const val PUSH_CONFIG_INCLUDE_UNIFIED_PUSH = true + const val PUSH_CONFIG_INCLUDE_FIREBASE: Boolean = true + const val PUSH_CONFIG_INCLUDE_UNIFIED_PUSH: Boolean = true + val PUSHER_APP_ID_RELEASE: String? = null + val PUSHER_APP_ID_DEBUG: String? = null + val PUSHER_APP_ID_NIGHTLY: String? = null } diff --git a/services/analytics/noop/src/main/kotlin/io/element/android/services/analytics/noop/di/NoopAnalyticsModule.kt b/services/analytics/noop/src/main/kotlin/io/element/android/services/analytics/noop/di/NoopAnalyticsBindingContainer.kt similarity index 85% rename from services/analytics/noop/src/main/kotlin/io/element/android/services/analytics/noop/di/NoopAnalyticsModule.kt rename to services/analytics/noop/src/main/kotlin/io/element/android/services/analytics/noop/di/NoopAnalyticsBindingContainer.kt index 3c4b00c7a0..d411926529 100644 --- a/services/analytics/noop/src/main/kotlin/io/element/android/services/analytics/noop/di/NoopAnalyticsModule.kt +++ b/services/analytics/noop/src/main/kotlin/io/element/android/services/analytics/noop/di/NoopAnalyticsBindingContainer.kt @@ -15,7 +15,7 @@ import io.element.android.libraries.di.identifiers.SentrySdkDsn @BindingContainer @ContributesTo(AppScope::class) -object NoopAnalyticsModule { +object NoopAnalyticsBindingContainer { @Provides - fun provideSentrySdkDsn(): SentrySdkDsn? = null + fun providesSentrySdkDsn(): SentrySdkDsn? = null } diff --git a/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/di/SentryModule.kt b/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/di/SentryBindingContainer.kt similarity index 73% rename from services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/di/SentryModule.kt rename to services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/di/SentryBindingContainer.kt index 74b7d56683..9e1ca7b886 100644 --- a/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/di/SentryModule.kt +++ b/services/analyticsproviders/sentry/src/main/kotlin/io/element/android/services/analyticsproviders/sentry/di/SentryBindingContainer.kt @@ -17,10 +17,10 @@ import io.element.android.services.analyticsproviders.sentry.SentryConfig @BindingContainer @ContributesTo(AppScope::class) -object SentryModule { +object SentryBindingContainer { @Provides - fun provideSentryDsn(): SentryDsn? = SentryConfig.DSN.takeIf { it.isNotBlank() }?.let(::SentryDsn) + fun providesSentryDsn(): SentryDsn? = SentryConfig.DSN.takeIf { it.isNotBlank() }?.let(::SentryDsn) @Provides - fun provideSentrySdkDsn(): SentrySdkDsn? = SentryConfig.SDK_DSN.takeIf { it.isNotBlank() }?.let(::SentrySdkDsn) + fun providesSentrySdkDsn(): SentrySdkDsn? = SentryConfig.SDK_DSN.takeIf { it.isNotBlank() }?.let(::SentrySdkDsn) } diff --git a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/di/AppNavStateModule.kt b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/di/AppNavStateBindingContainer.kt similarity index 92% rename from services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/di/AppNavStateModule.kt rename to services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/di/AppNavStateBindingContainer.kt index 4cf9c25b7b..c7ffa7aab2 100644 --- a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/di/AppNavStateModule.kt +++ b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/di/AppNavStateBindingContainer.kt @@ -20,9 +20,9 @@ import io.element.android.services.appnavstate.impl.initializer.AppForegroundSta @BindingContainer @ContributesTo(AppScope::class) -object AppNavStateModule { +object AppNavStateBindingContainer { @Provides - fun provideAppForegroundStateService( + fun providesAppForegroundStateService( @ApplicationContext context: Context ): AppForegroundStateService = AppInitializer.getInstance(context).initializeComponent(AppForegroundStateServiceInitializer::class.java) diff --git a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/TimeModule.kt b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/di/TimeBindingContainer.kt similarity index 79% rename from services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/TimeModule.kt rename to services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/di/TimeBindingContainer.kt index 766a63ebfd..fad0e4a3ab 100644 --- a/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/TimeModule.kt +++ b/services/toolbox/impl/src/main/kotlin/io/element/android/services/toolbox/impl/systemclock/di/TimeBindingContainer.kt @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.services.toolbox.impl.systemclock +package io.element.android.services.toolbox.impl.systemclock.di import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.BindingContainer @@ -16,9 +16,9 @@ import kotlin.time.TimeSource @BindingContainer @ContributesTo(AppScope::class) -object TimeModule { +object TimeBindingContainer { @Provides - fun timeSource(): TimeSource { + fun providesTimeSource(): TimeSource { return TimeSource.Monotonic } } diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistArchitectureTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistArchitectureTest.kt index c0e62482fa..26f9ef50a3 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistArchitectureTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistArchitectureTest.kt @@ -49,12 +49,12 @@ class KonsistArchitectureTest { } @Test - fun `Events MUST be sealed interface`() { + fun `Event MUST be sealed interface`() { Konsist.scopeFromProject() .classes() .withSealedModifier() - .withNameEndingWith("Events") - .assertEmpty(additionalMessage = "Events class MUST be sealed interface") + .withNameEndingWith("Event") + .assertEmpty(additionalMessage = "Event class MUST be sealed interface") } @Test diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt index 93b3200acc..53cdbbf225 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt @@ -57,16 +57,16 @@ class KonsistClassNameTest { } @Test - fun `Classes extending 'PreviewParameterProvider' name MUST end with 'Provider' and MUST contain provided class name`() { + fun `Classes extending 'PreviewParameterProvider' name MUST end with 'PreviewParam' and MUST contain provided class name`() { Konsist.scopeFromProduction() .classes() .withAllParentsOf(PreviewParameterProvider::class) .withoutName( - "AspectRatioProvider", - "EditableAvatarViewUriProvider", - "LoginModeViewErrorProvider", - "OverlapRatioProvider", - "TextFileContentProvider", + "AspectRatioPreviewParam", + "EditableAvatarViewUriPreviewParam", + "LoginModeViewErrorPreviewParam", + "OverlapRatioPreviewParam", + "TextFileContentPreviewParam", ) .also { // Check that classes are actually found @@ -82,8 +82,7 @@ class KonsistClassNameTest { .removeSuffix("?") .replace(".", "") val name = klass.name - name.endsWith("Provider") && - name.endsWith("PreviewProvider").not() && + name.endsWith("PreviewParam") && name.contains(providedType) } } @@ -104,6 +103,7 @@ class KonsistClassNameTest { "FakeImageLoader", "FakeKeyStore", "FakeListenableFuture", + "FakeParentNode", ) .assertTrue { val interfaceName = it.name diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistImmutableTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistImmutableTest.kt index ceb0058124..8534c50b6e 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistImmutableTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistImmutableTest.kt @@ -67,11 +67,14 @@ class KonsistImmutableTest { } @Test - fun `Immutable annotation is not used on sealed interface for Presenter Events`() { + fun `Immutable annotation is not used on sealed interface for Presenter Event`() { Konsist .scopeFromProduction() .interfaces() - .withNameEndingWith("Events") + .withoutName( + "LatestEvent", + ) + .withNameEndingWith("Event") .withAnnotationOf(Immutable::class) .assertEmpty() } diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/node/FakeNodeFactoriesBindings.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/node/FakeNodeFactoriesBindings.kt new file mode 100644 index 0000000000..cee2c74960 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/node/FakeNodeFactoriesBindings.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.tests.testutils.node + +import com.bumble.appyx.core.node.Node +import io.element.android.libraries.architecture.AssistedNodeFactory +import io.element.android.libraries.architecture.NodeFactoriesBindings +import kotlin.reflect.KClass + +class FakeNodeFactoriesBindings( + private val nodeFactories: Map, AssistedNodeFactory<*>>, +) : NodeFactoriesBindings { + override fun nodeFactories() = nodeFactories +} diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/node/FakeParentNode.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/node/FakeParentNode.kt new file mode 100644 index 0000000000..1375043d48 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/node/FakeParentNode.kt @@ -0,0 +1,34 @@ +/* + * 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.tests.testutils.node + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.node.ParentNode +import com.bumble.appyx.core.node.node +import io.element.android.libraries.di.DependencyInjectionGraphOwner + +/** + * A Node creates its children using the dependency injection graph of its ancestors, so a Node under test + * needs such a parent to be able to create its children. + */ +class FakeParentNode( + override val graph: Any, +) : ParentNode( + navModel = PermanentNavModel(navTargets = setOf(Unit), savedStateMap = null), + buildContext = BuildContext.root(savedStateMap = null), +), + DependencyInjectionGraphOwner { + override fun resolve(navTarget: Unit, buildContext: BuildContext): Node = node(buildContext) {} + + @Composable + override fun View(modifier: Modifier) = Unit +} diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/MutablePresenter.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/presenter/MutablePresenter.kt similarity index 93% rename from tests/testutils/src/main/kotlin/io/element/android/tests/testutils/MutablePresenter.kt rename to tests/testutils/src/main/kotlin/io/element/android/tests/testutils/presenter/MutablePresenter.kt index ae8768e0b0..13fcc920c2 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/MutablePresenter.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/presenter/MutablePresenter.kt @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.tests.testutils +package io.element.android.tests.testutils.presenter import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/presenter/NotUsedPresenter.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/presenter/NotUsedPresenter.kt new file mode 100644 index 0000000000..bd786c2484 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/presenter/NotUsedPresenter.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.tests.testutils.presenter + +import androidx.compose.runtime.Composable +import io.element.android.libraries.architecture.Presenter + +/** + * A presenter that cannot be used, util for testing Nodes. + */ +class NotUsedPresenter : Presenter { + @Composable + override fun present(): T = error("Not used") +} diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_0_en.png new file mode 100644 index 0000000000..bd73e322ab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 +size 3657 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_1_en.png new file mode 100644 index 0000000000..955af7bb9a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a856467fba0134aefcad2428493ce9169fccf90705b71d540c2eed935d9b625e +size 22639 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_2_en.png new file mode 100644 index 0000000000..a4fa915963 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70386b8f6a8998f246819c6d643543babcf41ba72db86db40559a755876c7650 +size 12144 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_3_en.png new file mode 100644 index 0000000000..25a66543da --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18498dbff28491dd108900453eab13d8224aa7960eefbe25993dcc82198677dd +size 35825 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_4_en.png new file mode 100644 index 0000000000..be7992706c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:346946f668bf18908480bf6b97158adcd7d9099d4be111055412ac2d4b7094c9 +size 77330 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_5_en.png new file mode 100644 index 0000000000..ca2936923c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc3946915978e17cf404469273a5ff174c12f53da8fc3cff53ac6df8721e0182 +size 23522 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_0_en.png new file mode 100644 index 0000000000..bd73e322ab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 +size 3657 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_1_en.png new file mode 100644 index 0000000000..cfef0a6883 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad20c730bb320093a5c639938430e6851244e4425b46f3ae10262d9ca96215a4 +size 21972 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_2_en.png new file mode 100644 index 0000000000..ec806ea229 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb20125a38d4d9436bee64de8f8d09d4afa372a72b7e33325b8d69f5b5760cdd +size 11694 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_3_en.png new file mode 100644 index 0000000000..8dc241988a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7d01b051fda7a3f6dc7736bdbd631bd2e0f0cef61dbd9c973b48cee1e69d56c +size 35624 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_4_en.png new file mode 100644 index 0000000000..8e618f0070 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6637c79e684bf350204756a93679163fb17b8cf5f54c835fe81464d644edc3ff +size 75840 diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_5_en.png new file mode 100644 index 0000000000..2864a9ffc8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e305c212a008854c19255f91824631d55d9fb523799ecad9b50039aa8b178323 +size 23053 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en.png deleted file mode 100644 index c9bf9e3c1c..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0990301d92d4862054ba5e54103dac359df37997242498766854d7bb13431657 -size 6068 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en.png deleted file mode 100644 index fb829c4873..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3022b2b813d9a3f77b0ba72706f0fb870a1c3f7b4a8efc90e611b66b6e9bdf8e -size 5926 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en.png deleted file mode 100644 index 23858ce575..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c7544e2566517e8692a1cccd4607bc2ab0f2767bbe9e5b39eb439d7695ad2ce5 -size 46905 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en.png deleted file mode 100644 index 9b505f3cf9..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b5dbf45467305e83f302fd626a2c0adcd27eac6aeea8b7a83efdb6e728d974da -size 44703 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en.png deleted file mode 100644 index 815986cbc3..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:41907de78c86fca804af2af7dd351db6a1ab87515e71bb906ffa2219ad4f5879 -size 45880 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en.png deleted file mode 100644 index f08662715a..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:62581b532695d10a42432dfcb37249c28c2bd617ecb49d7f3a8224855b78ae55 -size 43600 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en.png index 721b1f8389..caa1dc1cbb 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fb394bf8c063e5b4457fc75c171849b1ae7f435941e0d86d09298a846595861 -size 35160 +oid sha256:da11677aad93fcf9461c62a0fb9a1f384950dc69f0666914da5785265d56a4dd +size 35877 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en.png index 3d04fae696..cb6f870735 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4626f30754044cee4438c44f8972f1b643342aea9c7b38f3650874da8c7e7614 -size 37303 +oid sha256:99d28d3547d50e9ae23b012521ef5f8a157aa4b3a8a98b3e138847652f8cacff +size 35761 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en.png index c0d9a71d1d..d9eaefae3c 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ccaed5beab96b32c7ba783136d1fe8483697de44d0ed7c7555cbef9dd3708e66 -size 38953 +oid sha256:84954d900742e981811f1471fbe175f57c927ffc4da9e801d5e5b48f073dc13a +size 39610 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_3_en.png new file mode 100644 index 0000000000..aa987474a3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a524b0538ad4204ea66c46c139035671f55d874c45d46476fb885c15879b93f2 +size 31806 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en.png index 0127885711..0b0b5f123c 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ae87930bb1123d432f7f4e2792ad1e372ec383f968ad335b4f2f069b8faa307 -size 34294 +oid sha256:bb5607ed2e0986d5035120a2e6276ff33e65a7bafa5325a0c0c22f1975b69c2b +size 34986 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en.png index b15dcf0142..4da08d87a3 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:787b3708bfc0bebd19cde31589e175ee0ea593c4bd1461b9593e9705947e6ffc -size 36280 +oid sha256:80c7e798346755382f913f72da9b18fc200867280e6f33adde3622a964f7d2d7 +size 34883 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en.png index b9046df90d..3d6c836738 100644 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8d1a1013e5eaed74889193e4c6551ae5afa96a1d10fa3d32de3dc96a0c1ae13 -size 36582 +oid sha256:cbe35c9bb9ec509b64a2ce27afa879b8904269ae8f9afbd5ac8a91098d7fccb3 +size 38777 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_3_en.png new file mode 100644 index 0000000000..bada75332c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efa31bac320466c41fbf33b462418716c1f4fb6def584b38834320d213d2c819 +size 29777 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_0_en.png deleted file mode 100644 index 34cf030cba..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:09bf68ea40f2b1c109b015b8e9e9c767931ef70936a956652ac649b8e5a2561e -size 13491 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_1_en.png deleted file mode 100644 index 67186daea6..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:323218a1ce6e4afaf4632fed871cbc6e17ed6686243523aeb9da0d81a7825d31 -size 13602 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_2_en.png deleted file mode 100644 index c0cc08611c..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:193e7af1adc71d5e1133694e234671ebb6fefe3766c9891733c3ae46a999b1cd -size 13655 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_3_en.png deleted file mode 100644 index 26e32e2174..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb76a05a00c3e41c7edaa853db6d3773fddfb23c750eea4f45d4f4b1acfe5447 -size 15466 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_0_en.png deleted file mode 100644 index da53968272..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3a153cb8df9aadaa9cee0fef7f140e43f33851f18815674edc9a9682a7c601d2 -size 12942 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_1_en.png deleted file mode 100644 index 5df3ef308b..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cba9108df3dae3f9968825e13528087fdf5d8b70c03faabbf0b06e276392a7d7 -size 13048 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_2_en.png deleted file mode 100644 index 91a5e6418a..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_2_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:79cd361b36c8be418871a8cf6773bcc8f3626fc8d09012340f6d245ea6b0574e -size 12967 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_3_en.png deleted file mode 100644 index c6b38f80b0..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_3_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9063e4d7b900c397dacd964c25ce42865e292ea2a5a41f37cb7dfabd86f9246f -size 14142 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en.png deleted file mode 100644 index 86e3a6a0c1..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:64ca7f84a90416cac5161e379079edb3edadf1f82240214974a3216cd5d41090 -size 23418 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en.png deleted file mode 100644 index 583a7beb59..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bd5189b1079f9011162ff7ba2c9211fbfff543db892b86ea3b57ec78495d2019 -size 50344 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en.png deleted file mode 100644 index fd9fce9c5c..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5122ee1af4ff4ddc5e4c766f992e9833e4c1329bc634e82b0e52743326328447 -size 22757 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en.png deleted file mode 100644 index 1be9bd5d99..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:96742102f57f2e58027e3402ea01c91ee1eab6010dc46ef8ff8ee3175264a628 -size 49259 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_0_en.png new file mode 100644 index 0000000000..bd73e322ab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 +size 3657 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_1_en.png new file mode 100644 index 0000000000..98a271f16e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3412f2c40e8bff9f152ad856050415a16b76376d46eda582168f75e126485676 +size 21803 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_2_en.png new file mode 100644 index 0000000000..de396af500 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec927c9bd1b1e0000521d476f3141a6d2912b558ee1fddd089936afd59ac4d27 +size 20905 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_3_en.png new file mode 100644 index 0000000000..3edc968290 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:346138ce103462468652d51f969514a17b096795163ce1d2be2a99adf61471f4 +size 21766 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_4_en.png new file mode 100644 index 0000000000..74989de70a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1afe321e7c1f2a57d3317e952975655f89a4acae1386b425be089e03cc03a8d9 +size 22697 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_5_en.png new file mode 100644 index 0000000000..7bd3c0d04f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aba767b7b1380ca1fcd49498376128b5e537da7f15b2512c6f6cf409b3a3f9df +size 19567 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_0_en.png new file mode 100644 index 0000000000..bd73e322ab --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 +size 3657 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_1_en.png new file mode 100644 index 0000000000..b1f589b037 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05211670ecfed13426dae281438c0f51d4d98b1d33178ac1e3753bc36855b952 +size 20362 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_2_en.png new file mode 100644 index 0000000000..c2ff59b834 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9417e640c72190934981c3d7f29e9b60b241c69bcab800af7908b6b5d097089a +size 19453 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_3_en.png new file mode 100644 index 0000000000..7aa92967f6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e38f946dfda1008370b1171f4daffb2afea93b6f8ba9d8e816b8a12124467087 +size 20365 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_4_en.png new file mode 100644 index 0000000000..c5e55379a8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3996b007ecb0d6ccbba7b21d35a089b5ea5f0a314fe8a09b9f88e3829272a715 +size 21290 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_5_en.png new file mode 100644 index 0000000000..5270ee0b32 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d7b95deec3bb0c5b542885a8900144233434561b5488e50c1560cdb287b9a61 +size 18154 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesViewA11y_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesViewA11y_en.png index f47e74ab7b..3c357233b9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesViewA11y_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesViewA11y_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5823ae9a382421609200d569799505706180b94c6923f315caa81f8767ac58f8 -size 131340 +oid sha256:95c5746de02c584536b1178614d15460bbdda633ddf44485261007618bd99f50 +size 132268 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_0_en.png index 43a375ae4b..dbe9e3ffb5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9148c575c023d043014186ce8e4bdd30040d3ba2bb66ab8bffebe9ecbbf5bbbe -size 5424 +oid sha256:9a3daec0fbcae2ee6e2aa953bd0959bb3b043a586b47eacfc9b02ef9e1159c1a +size 16358 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_1_en.png index 3132156859..96deb3b002 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c7f1be3a4de422913deee95b1854300e0da3c059bc22059e76fc8744ebd80de -size 7679 +oid sha256:c2a053aad5bb7c68b1c294f6c367b4299c7c5200a5e6df0a34c19f37d925cb8f +size 24315 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_2_en.png index 3e1099a271..c63a0305dc 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:452fbf2b5f3ab038bd057a6df50a0618ee42e85eb963d1e8f93ceb1231e425dd -size 6106 +oid sha256:c79fcd68bdb06005b5ac50d4a675553bf5a3ea591d5515af5e6b6ec5a1c8853f +size 23265 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_3_en.png index 9e256f3b40..c63a0305dc 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f9e881db2cf82e859e95b607e4c7c874a21f16813b7775d456601488d147920 -size 5602 +oid sha256:c79fcd68bdb06005b5ac50d4a675553bf5a3ea591d5515af5e6b6ec5a1c8853f +size 23265 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_4_en.png index 624a6ce7e6..a2d4c7fce5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c868d2d4d799bb09b48e8944041ae2183d5142e41dc328f77c28e57be301564 -size 7442 +oid sha256:f1dd9fb953723fa1295dd01428d664fa13e27d936dd88ad8c0036de75f62e175 +size 17745 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_5_en.png index b62391bd05..c5c69b6c97 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35a8d9b3f96847fa3f5d322c65e773a195434fe5d41d2a6c84521b15a361f1f3 -size 6051 +oid sha256:715a91c80c93b799d9c6ca1e1322a3c919b3d001045bf1cf2b2a2833e1727853 +size 17360 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_6_en.png index 40da538fb4..8ddef26592 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c97fce3b0e863027528727956e3b3d14f1970b047ec03777d2ec843eac20af3 -size 6066 +oid sha256:d2e44c59ed3b3ca08a986966a711ec9ab0eca1277d24b5bce50130331c2f1070 +size 25443 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_7_en.png deleted file mode 100644 index 4535def866..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_7_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:992772b9d79a19c8cc8c6f35d6ffc0778c1a9816357ecaa678d4d15e1cd28884 -size 8441 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_8_en.png deleted file mode 100644 index 9418095e22..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Day_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:07c16f4e100a8534a5a1535af50f5d7b77ae1ad6ff5e6e73a260896ebe5ff054 -size 6569 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_0_en.png index 400724a0d6..d51245a8d6 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13276bd3b6f55624e00d723871124af7c27fa662f5edf58efbd58a69abbe11de -size 5554 +oid sha256:932f8f742c7f123fa8336f5813d561f7fb5f66235eccf2277ee21d1ed66a6cc4 +size 15888 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_1_en.png index 020b2418ea..a11c62d653 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a87b60e1b304f4712481a6984e1673ca7ac0608dad31b96ce57bd20345a99a4 -size 7748 +oid sha256:77feb0d4e70c3773a5bc1dcd5cdc847171ce7c8810d91344284a40a3b0585d14 +size 23866 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_2_en.png index b808316f7f..f478607f50 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46734678bb9267ada175d62566384d50be7433e688ffc83ef3068f261ac436ec -size 6213 +oid sha256:8a7dd412c7dc6a631c9d8cabb96122f6317fb660d03dee3561c205513a7ad887 +size 23061 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_3_en.png index fc3f3e27bb..f478607f50 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65f2332f2e34ef9f91442d1a785efec020543cf85e42a165fc88f2df9faf5794 -size 5543 +oid sha256:8a7dd412c7dc6a631c9d8cabb96122f6317fb660d03dee3561c205513a7ad887 +size 23061 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_4_en.png index 69c304a8f6..e95f23195a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07f20243dab69d313d92d7681d6448e72a42c08cbf9f572e97c17a3e9204d3a1 -size 7329 +oid sha256:e473ba8f9d57ab493a8e7cba25b8a1bf3ec1dab0216bc75b3a90faf0e7d15285 +size 17312 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_5_en.png index 91a9fa1ac6..3f546d4195 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:042ef4ee70d8508023294ea64d5e113d4aec186d016a14338f25780f452b692e -size 5983 +oid sha256:e08e1a0152ed654f9cdd14ea53e21c3650f42e758a13a229e12f2927867cbe22 +size 16901 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_6_en.png index ef41786063..cd93e59759 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e947db105c1b42d1325625879dd1235a19cac1ce1be62755ab43c4ef6b46911 -size 6026 +oid sha256:0ea4c96a40dedcbe9ba595ac5d5d0f303a1945797beef705f2e940d25c06ee54 +size 25149 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_7_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_7_en.png deleted file mode 100644 index 3a7bd28ed5..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_7_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d62d917c760629360fae73b094030e1669e6dcbd959861cdf2fe70d89ad9ff45 -size 8310 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_8_en.png deleted file mode 100644 index 1f2b7ed693..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.sender_SenderName_Night_8_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9547bc9a1d795d3b7eda0759abdc3b0c65f4b8db102be35d51968b9c0e942351 -size 6553 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en.png index 96215547bb..bae0e74d1e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1622f9765fda65985a369d6958f7a5993696dd629c0c1013c0a1647cfb7b562 -size 50109 +oid sha256:25fdfa4b65263c02e10c6ddea16a3210a84722a742a66cd2d91b8b3f61968466 +size 49924 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en.png index 70d41498a8..389ae383c8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a94de003f65d2fb30ab42e4ac60802707abf1358b0defed7598a3aad69992f40 -size 48136 +oid sha256:dc99c7505aa48b7ccdbae1de6daec75c12b9293172c804ef3cf027f6b46c45ea +size 47883 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png index c17cbfdb66..45f5ce2bde 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fdab4bea33337547250355c29991f45c118e8c2c05abdf69b1824475feced5d6 -size 51241 +oid sha256:c446e3b322a1eb8634f2f643819672cfb626ca9e76c7e1fb5a10fc03a1ae7824 +size 51031 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png index ef9c49d6c5..ff238fd979 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f92c594febc463911c8418deeff3b94e5a8ce4171461d812a0f4f527ab0cf8e -size 49245 +oid sha256:86260ffd841034c31327df736e0e127271f875a6cf383c688b7ea8c3a9d1dbd4 +size 48943 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png index 9ba3d99527..cebea0c88b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81d8acdb1158c3e5c19b5a602e3b9afe7643f1bb77d43f51d3f5c4327478a76a -size 50282 +oid sha256:9669ba8ab19e7f7f1322e973e6f39855113b45ab5f6b26e7b5230a2fe9ec1d68 +size 50067 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png index 969e5b04bf..50cd50736a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c225ad12c5a32e2392606f9afacc6dbc37478a141d5485e4942012c6b22ad052 -size 48046 +oid sha256:f1f66076e3b06df1471f367bed57f81d15272e3600eebc51cd131794e29d5dfb +size 47767 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en.png index a0192f0890..c4b21b86af 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cfb1d8b7828692d5c318206770f9484822a1749b3e80a618b502811cf512d76 -size 60489 +oid sha256:ffe11074abe52ccd1c34f687f780c5185be370b2b92867ee0b686bad85f28890 +size 60210 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en.png index c745fa256a..839c4a88ba 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca67409e4af247b43d8594d6351419cd12894485357596aff7b26745e2df009a -size 57687 +oid sha256:e9914f3ffd70dd83013ff6059539bda348ae64fc39a2419d621f8d198f6471cc +size 57380 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Day_0_en.png index 635e14be9e..f47016a59f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b3a91d104a5949dde83ac38b3127afa342816c631a1e29d78e2ada1bcc906bb -size 50150 +oid sha256:895ba0487ed933f4c8eddc99fe008032fe01454771ccf2987905aec357f3d9c0 +size 49972 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Night_0_en.png index 573b6123ed..adfcb6a616 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69c281829fbca15261f196d2134f933b789ff444cd7bebc88d05cdbb7553357f -size 48012 +oid sha256:b5a435c5aa78ffda52d96d611ab869d006e6c95e8aaf4cd005d99128b4f60b1c +size 47736 diff --git a/tools/adb/force_stop.sh b/tools/adb/force_stop.sh new file mode 100755 index 0000000000..aaf9274010 --- /dev/null +++ b/tools/adb/force_stop.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# 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_name="io.element.android.x.debug" + +echo " => Force stop ${package_name}" + +adb shell am force-stop "${package_name}" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 8a7790be1e..8bd6f14c12 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -173,6 +173,7 @@ "includeRegex" : [ "screen_onboarding_.*", "screen\\.onboarding\\..*", + "screen\\.change_server\\..*", "screen\\.missing_key_backup\\..*", "screen_login_.*", "screen_server_confirmation_.*", @@ -271,6 +272,7 @@ "screen_room_retry.*", "screen_room_timeline.*", "screen\\.room\\.timeline.*", + "error\\.sending_failure\\..*", "screen\\.room_timeline.*", "screen_room_typing.*", "screen\\.image_edition\\..*", diff --git a/tools/manifest/gplay/release/aaptDump.txt b/tools/manifest/gplay/release/aaptDump.txt index fcb3669a59..7eae1a5a78 100644 --- a/tools/manifest/gplay/release/aaptDump.txt +++ b/tools/manifest/gplay/release/aaptDump.txt @@ -1,4 +1,4 @@ -package: name='io.element.android.x' versionCode='202608010' versionName='26.08.1' platformBuildVersionName='17' platformBuildVersionCode='37' compileSdkVersion='37' compileSdkVersionCodename='17' +package: name='io.element.android.x' versionCode='202608020' versionName='26.08.2' platformBuildVersionName='17' platformBuildVersionCode='37' compileSdkVersion='37' compileSdkVersionCodename='17' sdkVersion:'24' targetSdkVersion:'37' uses-permission: name='android.permission.POST_NOTIFICATIONS' diff --git a/tools/release/fix-pg-map-id.py b/tools/release/fix-pg-map-id.py deleted file mode 100644 index c37fec2528..0000000000 --- a/tools/release/fix-pg-map-id.py +++ /dev/null @@ -1,227 +0,0 @@ -#!/usr/bin/python3 -# encoding: utf-8 -# SPDX-FileCopyrightText: 2024 FC (Fay) Stegerman -# SPDX-License-Identifier: GPL-3.0-or-later. - -import hashlib -import os -import re -import struct -import zipfile -import zlib - -from binascii import hexlify -from typing import Any, Dict, Match, Tuple - -DEX_MAGIC = b"dex\n" -DEX_MAGIC_RE = re.compile(rb"dex\n(\d{3})\x00") - -PROF_MAGIC = b"pro\x00" -PROF_010_P = b"010\x00" - -CLASSES_DEX_RE = re.compile(r"classes\d*\.dex") -ASSET_PROF = "assets/dexopt/baseline.prof" - -PG_MAP_ID_RE = re.compile(rb'(~~R8{"backend":"dex".*?"pg-map-id":")([0-9a-f]{7})(")') - -ATTRS = ("compress_type", "create_system", "create_version", "date_time", - "external_attr", "extract_version", "flag_bits") -LEVELS = (9, 6, 4, 1) - - -class Error(RuntimeError): - pass - - -# FIXME: is there a better alternative? -class ReproducibleZipInfo(zipfile.ZipInfo): - """Reproducible ZipInfo hack.""" - - if "_compresslevel" not in zipfile.ZipInfo.__slots__: # type: ignore[attr-defined] - if "compress_level" not in zipfile.ZipInfo.__slots__: # type: ignore[attr-defined] - raise Error("zipfile.ZipInfo has no ._compresslevel") - - _compresslevel: int - _override: Dict[str, Any] = {} - - def __init__(self, zinfo: zipfile.ZipInfo, **override: Any) -> None: - # pylint: disable=W0231 - if override: - self._override = {**self._override, **override} - for k in self.__slots__: - if hasattr(zinfo, k): - setattr(self, k, getattr(zinfo, k)) - - def __getattribute__(self, name: str) -> Any: - if name != "_override": - try: - return self._override[name] - except KeyError: - pass - return object.__getattribute__(self, name) - - -def fix_pg_map_id(input_dir: str, output_dir: str, map_id: str) -> None: - file_data = {} - for filename in [ASSET_PROF] + sorted(os.listdir(input_dir)): - if re.fullmatch(CLASSES_DEX_RE, filename) or filename == ASSET_PROF: - print(f"reading {filename!r}...") - with open(os.path.join(input_dir, *filename.split("/")), "rb") as fh: - file_data[filename] = fh.read() - _fix_pg_map_id(file_data, map_id) - for filename, data in file_data.items(): - print(f"writing {filename!r}...") - if "/" in filename: - os.makedirs(os.path.join(output_dir, *filename.split("/")[:-1]), exist_ok=True) - with open(os.path.join(output_dir, *filename.split("/")), "wb") as fh: - fh.write(data) - - -def fix_pg_map_id_apk(input_apk: str, output_apk: str, map_id: str) -> None: - with open(input_apk, "rb") as fh_raw: - with zipfile.ZipFile(input_apk) as zf_in: - with zipfile.ZipFile(output_apk, "w") as zf_out: - file_data = {} - for info in zf_in.infolist(): - if re.fullmatch(CLASSES_DEX_RE, info.filename) or info.filename == ASSET_PROF: - print(f"reading {info.filename!r}...") - file_data[info.filename] = zf_in.read(info) - _fix_pg_map_id(file_data, map_id) - for info in zf_in.infolist(): - attrs = {attr: getattr(info, attr) for attr in ATTRS} - zinfo = ReproducibleZipInfo(info, **attrs) - if info.compress_type == 8: - fh_raw.seek(info.header_offset) - n, m = struct.unpack(" 0: - ccrc = zlib.crc32(fh_raw.read(min(size, 4096)), ccrc) - size -= 4096 - with zf_in.open(info) as fh_in: - comps = {lvl: zlib.compressobj(lvl, 8, -15) for lvl in LEVELS} - ccrcs = {lvl: 0 for lvl in LEVELS} - while True: - data = fh_in.read(4096) - if not data: - break - for lvl in LEVELS: - ccrcs[lvl] = zlib.crc32(comps[lvl].compress(data), ccrcs[lvl]) - for lvl in LEVELS: - if ccrc == zlib.crc32(comps[lvl].flush(), ccrcs[lvl]): - zinfo._compresslevel = lvl - break - else: - raise Error(f"Unable to determine compresslevel for {info.filename!r}") - elif info.compress_type != 0: - raise Error(f"Unsupported compress_type {info.compress_type}") - if re.fullmatch(CLASSES_DEX_RE, info.filename) or info.filename == ASSET_PROF: - print(f"writing {info.filename!r}...") - zf_out.writestr(zinfo, file_data[info.filename]) - else: - with zf_in.open(info) as fh_in: - with zf_out.open(zinfo, "w") as fh_out: - while True: - data = fh_in.read(4096) - if not data: - break - fh_out.write(data) - - -def _fix_pg_map_id(file_data: Dict[str, bytes], map_id: str) -> None: - crcs = {} - for filename in file_data: - if re.fullmatch(CLASSES_DEX_RE, filename): - print(f"fixing {filename!r}...") - data = _fix_dex_id_checksum(file_data[filename], map_id.encode()) - file_data[filename] = data - crcs[filename] = zlib.crc32(data) - if ASSET_PROF in file_data: - print(f"fixing {ASSET_PROF!r}...") - file_data[ASSET_PROF] = _fix_prof_checksum(file_data[ASSET_PROF], crcs) - - -def _fix_dex_id_checksum(data: bytes, map_id: bytes) -> bytes: - def repl(m: Match[bytes]) -> bytes: - print(f"fixing pg-map-id: {m.group(2)!r} -> {map_id!r}") - return m.group(1) + map_id + m.group(3) - - magic = data[:8] - if magic[:4] != DEX_MAGIC or not DEX_MAGIC_RE.fullmatch(magic): - raise Error(f"Unsupported magic {magic!r}") - print(f"dex version={int(magic[4:7]):03d}") - checksum, signature = struct.unpack(" {hexlify(fixed_sig).decode()}") - fixed_data = fixed_sig + fixed_data - fixed_checksum = zlib.adler32(fixed_data) - print(f"fixing checksum: 0x{checksum:x} -> 0x{fixed_checksum:x}") - return magic + int.to_bytes(fixed_checksum, 4, "little") + fixed_data - - -def _fix_prof_checksum(data: bytes, crcs: Dict[str, int]) -> bytes: - magic, data = _split(data, 4) - version, data = _split(data, 4) - if magic == PROF_MAGIC: - if version == PROF_010_P: - print("prof version=010 P") - return PROF_MAGIC + PROF_010_P + _fix_prof_010_p_checksum(data, crcs) - else: - raise Error(f"Unsupported prof version {version!r}") - else: - raise Error(f"Unsupported magic {magic!r}") - - -def _fix_prof_010_p_checksum(data: bytes, crcs: Dict[str, int]) -> bytes: - num_dex_files, uncompressed_data_size, compressed_data_size, data = _unpack(" 0x{fixed_checksum:x}") - dex_data_headers.append(struct.pack( - " Any: - assert all(c in " Tuple[bytes, bytes]: - return data[:size], data[size:] - - -if __name__ == "__main__": - import argparse - parser = argparse.ArgumentParser(prog="fix-pg-map-id.py") - parser.add_argument("input_dir_or_apk", metavar="INPUT_DIR_OR_APK") - parser.add_argument("output_dir_or_apk", metavar="OUTPUT_DIR_OR_APK") - parser.add_argument("pg_map_id", metavar="PG_MAP_ID") - args = parser.parse_args() - if os.path.isdir(args.input_dir_or_apk): - fix_pg_map_id(args.input_dir_or_apk, args.output_dir_or_apk, args.pg_map_id) - else: - fix_pg_map_id_apk(args.input_dir_or_apk, args.output_dir_or_apk, args.pg_map_id) - -# vim: set tw=80 sw=4 sts=4 et fdm=marker : diff --git a/tools/release/inplace-fix.py b/tools/release/inplace-fix.py deleted file mode 100644 index a791e69140..0000000000 --- a/tools/release/inplace-fix.py +++ /dev/null @@ -1,199 +0,0 @@ -#!/usr/bin/python3 -# encoding: utf-8 -# SPDX-FileCopyrightText: 2024 FC (Fay) Stegerman -# SPDX-License-Identifier: GPL-3.0-or-later. - -import argparse -import os -import shutil -import subprocess -import sys -import tempfile - -from typing import Optional, Tuple - -COMMANDS = ( - "fix-compresslevel", - "fix-files", - "fix-newlines", - "fix-pg-map-id", - "rm-files", - "sort-apk", - "sort-baseline", -) - -BUILD_TOOLS_WITH_BROKEN_ZIPALIGN = ("31.0.0", "32.0.0") -BUILD_TOOLS_WITH_PAGE_SIZE_FROM = "35.0.0-rc1" -SDK_ENV = ("ANDROID_HOME", "ANDROID_SDK", "ANDROID_SDK_ROOT") - - -def _zipalign_cmd(page_align: bool, page_size: Optional[int]) -> Tuple[str, ...]: - if page_align: - if page_size is not None: - return ("zipalign", "-P", str(page_size), "4") - return ("zipalign", "-p", "4") - return ("zipalign", "4") - - -ZIPALIGN = _zipalign_cmd(page_align=False, page_size=None) -ZIPALIGN_P = _zipalign_cmd(page_align=True, page_size=None) - - -class Error(RuntimeError): - pass - - -def inplace_fix(command: str, input_file: str, *args: str, - zipalign: bool = False, page_align: bool = False, - page_size: Optional[int] = None, internal: bool = False) -> None: - if command not in COMMANDS: - raise Error(f"Unknown command {command}") - exe, script = _script_cmd(command) - ext = os.path.splitext(input_file)[1] - with tempfile.TemporaryDirectory() as tdir: - fixed = os.path.join(tdir, "fixed" + ext) - run_command(exe, script, input_file, fixed, *args, trim=2) - if zipalign: - aligned = os.path.join(tdir, "aligned" + ext) - zac = zipalign_cmd(page_align=page_align, page_size=page_size, internal=internal) - run_command(*zac, fixed, aligned, trim=2) - print(f"[MOVE] {aligned} to {input_file}") - shutil.move(aligned, input_file) - else: - print(f"[MOVE] {fixed} to {input_file}") - shutil.move(fixed, input_file) - - -def zipalign_cmd(page_align: bool = False, page_size: Optional[int] = None, - internal: bool = False) -> Tuple[str, ...]: - """ - Find zipalign command using $PATH or $ANDROID_HOME etc. - - >>> zipalign_cmd() - ('zipalign', '4') - >>> zipalign_cmd(page_align=True) - ('zipalign', '-p', '4') - >>> zipalign_cmd(page_align=True, page_size=16) - ('zipalign', '-P', '16', '4') - >>> cmd = zipalign_cmd(page_align=True, page_size=16, internal=True) - >>> [x.split("/")[-1] for x in cmd] - ['python3', 'zipalign.py', '-P', '16', '4'] - >>> os.environ["PATH"] = "" - >>> for k in SDK_ENV: - ... os.environ[k] = "" - >>> cmd = zipalign_cmd() - >>> [x.split("/")[-1] for x in cmd] - ['python3', 'zipalign.py', '4'] - >>> os.environ["ANDROID_HOME"] = "test/fake-sdk" - >>> zipalign_cmd() - [SKIP BROKEN] 31.0.0 - [FOUND] test/fake-sdk/build-tools/30.0.3/zipalign - ('test/fake-sdk/build-tools/30.0.3/zipalign', '4') - >>> cmd = zipalign_cmd(page_align=True, page_size=16) - [SKIP TOO OLD] 31.0.0 - [SKIP TOO OLD] 30.0.3 - [SKIP TOO OLD] 26.0.2 - >>> [x.split("/")[-1] for x in cmd] - ['python3', 'zipalign.py', '-P', '16', '4'] - >>> os.environ["ANDROID_HOME"] = "test/fake-sdk-2" - >>> zipalign_cmd(page_align=True, page_size=16) - [FOUND] test/fake-sdk-2/build-tools/35.0.0-rc1/zipalign - ('test/fake-sdk-2/build-tools/35.0.0-rc1/zipalign', '-P', '16', '4') - - """ - cmd, *args = _zipalign_cmd(page_align, page_size) - if not internal: - if shutil.which(cmd): - return (cmd, *args) - for k in SDK_ENV: - if home := os.environ.get(k): - tools = os.path.join(home, "build-tools") - if os.path.exists(tools): - for vsn in sorted(os.listdir(tools), key=_vsn, reverse=True): - if page_size and _vsn(vsn) < _vsn(BUILD_TOOLS_WITH_PAGE_SIZE_FROM): - print(f"[SKIP TOO OLD] {vsn}") - continue - for s in BUILD_TOOLS_WITH_BROKEN_ZIPALIGN: - if vsn.startswith(s): - print(f"[SKIP BROKEN] {vsn}") - break - else: - c = os.path.join(tools, vsn, cmd) - if shutil.which(c): - print(f"[FOUND] {c}") - return (c, *args) - return (*_script_cmd(cmd), *args) - - -def _vsn(v: str) -> Tuple[int, ...]: - """ - >>> vs = "31.0.0 32.1.0-rc1 34.0.0-rc3 34.0.0 35.0.0-rc1".split() - >>> for v in sorted(vs, key=_vsn, reverse=True): - ... (_vsn(v), v) - ((35, 0, 0, 0, 1), '35.0.0-rc1') - ((34, 0, 0, 1, 0), '34.0.0') - ((34, 0, 0, 0, 3), '34.0.0-rc3') - ((32, 1, 0, 0, 1), '32.1.0-rc1') - ((31, 0, 0, 1, 0), '31.0.0') - """ - if "-rc" in v: - v = v.replace("-rc", ".0.", 1) - else: - v = v + ".1.0" - return tuple(int(x) if x.isdigit() else -1 for x in v.split(".")) - - -def _script_cmd(command: str) -> Tuple[str, str]: - script_dir = os.path.dirname(__file__) - for cmd in (command, command.replace("-", "_")): - script = os.path.join(script_dir, cmd + ".py") - if os.path.exists(script): - break - else: - raise Error(f"Script for {command} not found") - exe = sys.executable or "python3" - return exe, script - - -def run_command(*args: str, trim: int = 1) -> None: - targs = tuple(os.path.basename(a) for a in args[:trim]) + args[trim:] - print(f"[RUN] {' '.join(targs)}") - try: - subprocess.run(args, check=True) - except subprocess.CalledProcessError as e: - raise Error(f"{args[0]} command failed") from e - except FileNotFoundError as e: - raise Error(f"{args[0]} command not found") from e - - -def main() -> None: - prog = os.path.basename(sys.argv[0]) - usage = (f"{prog} [-h] [--zipalign] [--page-align] [--page-size N] [--internal]\n" - f"{len('usage: ' + prog) * ' '} COMMAND INPUT_FILE [...]") - epilog = f"Commands: {', '.join(COMMANDS)}." - parser = argparse.ArgumentParser(usage=usage, epilog=epilog) - parser.add_argument("--zipalign", action="store_true", - help="run zipalign after COMMAND") - parser.add_argument("--page-align", action="store_true", - help="run zipalign w/ -p option (implies --zipalign)") - parser.add_argument("--page-size", metavar="N", type=int, - help="run zipalign w/ -P N option (implies --page-align)") - parser.add_argument("--internal", action="store_true", - help="use zipalign.py instead of searching $PATH/$ANDROID_HOME/etc.") - parser.add_argument("command", metavar="COMMAND") - parser.add_argument("input_file", metavar="INPUT_FILE") - args, rest = parser.parse_known_args() - try: - inplace_fix(args.command, args.input_file, *rest, - zipalign=bool(args.zipalign or args.page_align or args.page_size), - page_align=bool(args.page_align or args.page_size), - page_size=args.page_size, internal=args.internal) - except Error as e: - print(f"Error: {e}.", file=sys.stderr) - sys.exit(1) - - -if __name__ == "__main__": - main() - -# vim: set tw=80 sw=4 sts=4 et fdm=marker : diff --git a/tools/release/release.sh b/tools/release/release.sh index 83ccc6d258..392074208a 100755 --- a/tools/release/release.sh +++ b/tools/release/release.sh @@ -225,15 +225,6 @@ printf "Unzipping the F-Droid artifact...\n" fdroidTargetPath="${targetPath}/fdroid" unzip "${targetPath}"/elementx-app-fdroid-apks-unsigned.zip -d "${fdroidTargetPath}" -printf "\n================================================================================\n" -printf "Patching the FDroid APKs using inplace-fix.py...\n" - -inplaceFixScript="./tools/release/inplace-fix.py" -python3 "${inplaceFixScript}" --page-size 16 fix-pg-map-id "${fdroidTargetPath}"/app-fdroid-arm64-v8a-release.apk '0000000' -python3 "${inplaceFixScript}" --page-size 16 fix-pg-map-id "${fdroidTargetPath}"/app-fdroid-armeabi-v7a-release.apk '0000000' -python3 "${inplaceFixScript}" --page-size 16 fix-pg-map-id "${fdroidTargetPath}"/app-fdroid-x86-release.apk '0000000' -python3 "${inplaceFixScript}" --page-size 16 fix-pg-map-id "${fdroidTargetPath}"/app-fdroid-x86_64-release.apk '0000000' - printf "\n================================================================================\n" printf "Signing the FDroid APKs...\n" diff --git a/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.0.kt b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.0.kt index 26372fc970..9230360bd9 100644 --- a/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.0.kt +++ b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.0.kt @@ -2,7 +2,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider -open class ${NAME}StateProvider : PreviewParameterProvider<${NAME}State> { +open class ${NAME}StatePreviewParam : PreviewParameterProvider<${NAME}State> { override val values: Sequence<${NAME}State> get() = sequenceOf( a${NAME}State(), diff --git a/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.2.kt b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.2.kt index c4f3b1b5b2..83e1b5c01f 100644 --- a/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.2.kt +++ b/tools/templates/files/fileTemplates/Template Presentation Classes.kt.child.2.kt @@ -27,7 +27,7 @@ fun ${NAME}View( @PreviewsDayNight @Composable internal fun ${NAME}ViewPreview( - @PreviewParameter(${NAME}StateProvider::class) state: ${NAME}State + @PreviewParameter(${NAME}StatePreviewParam::class) state: ${NAME}State ) = ElementPreview { ${NAME}View( state = state, diff --git a/tools/templates/files/options/file.template.settings.xml b/tools/templates/files/options/file.template.settings.xml index a284c63140..b45a92f4c2 100644 --- a/tools/templates/files/options/file.template.settings.xml +++ b/tools/templates/files/options/file.template.settings.xml @@ -2,13 +2,13 @@