diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index b200672f94..7f487a1e63 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(projects.libraries.pushproviders.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.matrixui) + implementation(projects.libraries.uiCommon) implementation(projects.libraries.uiStrings) implementation(projects.features.login.api) 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 a15c87ed77..66df9a6dd5 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -36,10 +36,8 @@ 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 -import dev.zacsweers.metro.Inject import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.annotations.ContributesNode import io.element.android.appnav.loggedin.LoggedInNode @@ -82,6 +80,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener import io.element.android.libraries.matrix.api.verification.VerificationRequest import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService +import io.element.android.libraries.ui.common.nodes.emptyNode import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first @@ -282,7 +281,7 @@ class LoggedInFlowNode( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.Placeholder -> createNode(buildContext) + NavTarget.Placeholder -> emptyNode(buildContext) NavTarget.LoggedInPermanent -> { val callback = object : LoggedInNode.Callback { override fun navigateToNotificationTroubleshoot() { @@ -549,13 +548,6 @@ class LoggedInFlowNode( } } -@ContributesNode(AppScope::class) -@Inject -class PlaceholderNode( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, -) : Node(buildContext, plugins = plugins) - @Parcelize private class AttachRoomOperation( val roomTarget: LoggedInFlowNode.NavTarget.Room, 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 d71fbf2eef..93df005c76 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -11,15 +11,11 @@ import android.content.Intent import android.os.Parcelable import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.node.node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.state.MutableSavedStateMap import com.bumble.appyx.navmodel.backstack.BackStack @@ -51,7 +47,6 @@ import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.waitForChildAttached import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.deeplink.api.DeeplinkData -import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator 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 @@ -61,6 +56,7 @@ import io.element.android.libraries.oidc.api.OidcAction import io.element.android.libraries.oidc.api.OidcActionFlow import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.ui.common.nodes.emptyNode import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -219,9 +215,10 @@ import timber.log.Timber override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { is NavTarget.LoggedInFlow -> { - val matrixClient = matrixSessionCache.getOrNull(navTarget.sessionId) ?: return splashNode(buildContext).also { - Timber.w("Couldn't find any session, go through SplashScreen") - } + val matrixClient = matrixSessionCache.getOrNull(navTarget.sessionId) + ?: return emptyNode(buildContext).also { + Timber.w("Couldn't find any session, go through SplashScreen") + } val inputs = LoggedInAppScopeFlowNode.Inputs(matrixClient) val callback = object : LoggedInAppScopeFlowNode.Callback { override fun onOpenBugReport() { @@ -252,7 +249,7 @@ import timber.log.Timber ) ).build() } - NavTarget.SplashScreen -> splashNode(buildContext) + NavTarget.SplashScreen -> emptyNode(buildContext) NavTarget.BugReport -> { val callback = object : BugReportEntryPoint.Callback { override fun onDone() { @@ -289,12 +286,6 @@ import timber.log.Timber } } - private fun splashNode(buildContext: BuildContext) = node(buildContext) { - Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() - } - } - suspend fun handleIntent(intent: Intent) { val resolvedIntent = intentResolver.resolve(intent) ?: return when (resolvedIntent) { diff --git a/features/ftue/impl/build.gradle.kts b/features/ftue/impl/build.gradle.kts index 8f940ed29a..d7d61f6d8d 100644 --- a/features/ftue/impl/build.gradle.kts +++ b/features/ftue/impl/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.libraries.preferences.api) + implementation(projects.libraries.uiCommon) implementation(projects.libraries.uiStrings) implementation(projects.libraries.testtags) implementation(projects.features.analytics.api) diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt index b625b07f12..6552a3b360 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt @@ -8,10 +8,7 @@ package io.element.android.features.ftue.impl import android.os.Parcelable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.modality.BuildContext @@ -20,10 +17,8 @@ import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.newRoot import com.bumble.appyx.navmodel.backstack.operation.replace -import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject -import dev.zacsweers.metro.Inject import io.element.android.annotations.ContributesNode import io.element.android.features.analytics.api.AnalyticsEntryPoint import io.element.android.features.ftue.impl.notifications.NotificationsOptInNode @@ -35,8 +30,8 @@ import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.ui.common.nodes.emptyNode import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -88,7 +83,7 @@ class FtueFlowNode( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Placeholder -> { - createNode(buildContext) + emptyNode(buildContext) } is NavTarget.SessionVerification -> { val callback = object : FtueSessionVerificationFlowNode.Callback { @@ -147,17 +142,3 @@ class FtueFlowNode( BackstackView() } } - -@ContributesNode(AppScope::class) -@Inject -class PlaceholderNode( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, -) : Node(buildContext, plugins = plugins) { - @Composable - override fun View(modifier: Modifier) { - Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() - } - } -} diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index 011d4919e7..998b763804 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation(projects.libraries.testtags) implementation(projects.libraries.uiUtils) implementation(projects.features.logout.api) + implementation(projects.libraries.uiCommon) implementation(projects.libraries.uiStrings) implementation(projects.libraries.sessionStorage.api) implementation(projects.services.appnavstate.api) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt index cfde6db84f..01aeaabe89 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/settings/LockScreenSettingsFlowNode.kt @@ -14,7 +14,6 @@ import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.node.node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.newRoot @@ -30,6 +29,7 @@ import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.ui.common.nodes.emptyNode import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @@ -42,7 +42,7 @@ class LockScreenSettingsFlowNode( private val pinCodeManager: PinCodeManager, ) : BaseFlowNode( backstack = BackStack( - initialElement = NavTarget.Unknown, + initialElement = NavTarget.Loading, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -50,7 +50,7 @@ class LockScreenSettingsFlowNode( ) { sealed interface NavTarget : Parcelable { @Parcelize - data object Unknown : NavTarget + data object Loading : NavTarget @Parcelize data object Unlock : NavTarget @@ -94,6 +94,9 @@ class LockScreenSettingsFlowNode( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { + NavTarget.Loading -> { + emptyNode(buildContext) + } NavTarget.Unlock -> { val callback = object : PinUnlockNode.Callback { override fun onUnlock() { @@ -113,7 +116,6 @@ class LockScreenSettingsFlowNode( } createNode(buildContext, plugins = listOf(callback)) } - NavTarget.Unknown -> node(buildContext) { } } } diff --git a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt index 3fec810cb2..3515e07a77 100644 --- a/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt +++ b/features/space/impl/src/main/kotlin/io/element/android/features/space/impl/SpaceFlowNode.kt @@ -19,7 +19,7 @@ import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.push import dev.zacsweers.metro.Assisted -import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.AssistedInject import io.element.android.annotations.ContributesNode import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.features.space.impl.leave.LeaveSpaceNode @@ -33,7 +33,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) -@Inject +@AssistedInject class SpaceFlowNode( @Assisted val buildContext: BuildContext, @Assisted plugins: List, 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 c7be10d956..9da27e244d 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 @@ -17,7 +17,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory -import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.AssistedInject import io.element.android.features.space.api.SpaceEntryPoint import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData @@ -35,7 +35,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlin.jvm.optionals.getOrNull -@Inject +@AssistedInject class LeaveSpacePresenter( @Assisted private val inputs: SpaceEntryPoint.Inputs, matrixClient: MatrixClient, diff --git a/libraries/ui-common/build.gradle.kts b/libraries/ui-common/build.gradle.kts new file mode 100644 index 0000000000..55ef7eaf49 --- /dev/null +++ b/libraries/ui-common/build.gradle.kts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.libraries.ui.common" +} + +dependencies { + implementation(libs.appyx.core) + implementation(projects.libraries.designsystem) +} diff --git a/libraries/ui-common/src/main/kotlin/io/element/android/libraries/ui/common/nodes/EmptyNode.kt b/libraries/ui-common/src/main/kotlin/io/element/android/libraries/ui/common/nodes/EmptyNode.kt new file mode 100644 index 0000000000..ab622b4b9d --- /dev/null +++ b/libraries/ui-common/src/main/kotlin/io/element/android/libraries/ui/common/nodes/EmptyNode.kt @@ -0,0 +1,44 @@ +/* + * 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.libraries.ui.common.nodes + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.node.node +import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight + +/** + * Ref: https://www.figma.com/design/0MMNu7cTOzLOlWb7ctTkv3/Element-X?node-id=1518-85323 + */ +fun emptyNode( + buildContext: BuildContext, +): Node = node(buildContext) { modifier -> + EmptyView(modifier) +} + +@Composable +private fun EmptyView( + modifier: Modifier = Modifier, +) = Box( + modifier = modifier + .fillMaxSize() + .background(ElementTheme.colors.bgCanvasDefault), +) + +@PreviewsDayNight +@Composable +internal fun EmptyViewPreview() = ElementPreview { + EmptyView(Modifier) +} diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistDiTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistDiTest.kt new file mode 100644 index 0000000000..093cfd4488 --- /dev/null +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistDiTest.kt @@ -0,0 +1,35 @@ +/* + * 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.tests.konsist + +import com.lemonappdev.konsist.api.Konsist +import com.lemonappdev.konsist.api.ext.list.withAnnotationOf +import com.lemonappdev.konsist.api.ext.list.withParameter +import com.lemonappdev.konsist.api.verify.assertTrue +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.Inject +import org.junit.Test + +class KonsistDiTest { + @Test + fun `class annotated with @Inject should not have constructors with @Assisted parameter`() { + Konsist + .scopeFromProject() + .classes() + .withAnnotationOf(Inject::class) + .assertTrue( + additionalMessage = "Class with @Assisted parameter in constructor should be annotated with @AssistedInject and not @Inject" + ) { classDeclaration -> + classDeclaration.constructors + .withParameter { parameterDeclaration -> + parameterDeclaration.hasAnnotationOf(Assisted::class) + } + .isEmpty() + } + } +} diff --git a/tests/uitests/src/test/snapshots/images/libraries.ui.common.nodes_EmptyView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.ui.common.nodes_EmptyView_Day_0_en.png new file mode 100644 index 0000000000..1b6fb4bab8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.ui.common.nodes_EmptyView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650 +size 3642 diff --git a/tests/uitests/src/test/snapshots/images/libraries.ui.common.nodes_EmptyView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.ui.common.nodes_EmptyView_Night_0_en.png new file mode 100644 index 0000000000..d6fd8eeb70 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.ui.common.nodes_EmptyView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd +size 3659