Show a loading indicator on the placeholders which can stay on screen forever

emptyNode() renders nothing at all, which is fine for a placeholder that is left
within a few frames, but makes the application look frozen when the wait can
last, or never end.

Add a delayBeforeShowingContent parameter to emptyNode(): when it is finite, a
progress indicator and a "Loading…" label fade in after that delay, reusing the
existing DelayedVisibility helper. It defaults to Duration.INFINITE, so the
splash screen keeps rendering an empty canvas.

Use a one second delay on the two placeholders which are not guaranteed to be
left: the FTUE one, rendered while waiting for the session verified status, and
the LoggedInFlowNode one, rendered while the FTUE state is unknown.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benoit Marty
2026-09-11 12:48:44 +02:00
parent 8101abf67d
commit 562e439447
4 changed files with 60 additions and 5 deletions
+1
View File
@@ -17,4 +17,5 @@ android {
dependencies {
implementation(libs.appyx.core)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
}
@@ -9,37 +9,84 @@
package io.element.android.libraries.ui.common.nodes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import 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
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.DelayedVisibility
import io.element.android.libraries.ui.strings.CommonStrings
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
/**
* Ref: https://www.figma.com/design/0MMNu7cTOzLOlWb7ctTkv3/Element-X?node-id=1518-85323
*
* @param buildContext the [BuildContext] of the Node.
* @param delayBeforeShowingContent when finite, a loading indicator fades in after this delay. Use it for
* placeholders which can stay on screen for a long time, or forever, else the application looks frozen.
* Defaults to [Duration.INFINITE], i.e. the Node stays empty.
*/
fun emptyNode(
buildContext: BuildContext,
delayBeforeShowingContent: Duration = Duration.INFINITE,
): Node = node(buildContext) { modifier ->
EmptyView(modifier)
EmptyView(
delayBeforeShowingContent = delayBeforeShowingContent,
modifier = modifier,
)
}
@Composable
private fun EmptyView(
delayBeforeShowingContent: Duration,
modifier: Modifier = Modifier,
) = Box(
modifier = modifier
.fillMaxSize()
.background(ElementTheme.colors.bgCanvasDefault),
)
contentAlignment = Alignment.Center,
) {
if (delayBeforeShowingContent.isFinite()) {
DelayedVisibility(duration = delayBeforeShowingContent) {
LoadingContent()
}
}
}
@Composable
private fun LoadingContent() = Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
CircularProgressIndicator()
Text(
text = stringResource(CommonStrings.common_loading),
color = ElementTheme.colors.textSecondary,
style = ElementTheme.typography.fontBodyMdRegular,
)
}
@PreviewsDayNight
@Composable
internal fun EmptyViewPreview() = ElementPreview {
EmptyView(Modifier)
EmptyView(delayBeforeShowingContent = Duration.INFINITE)
}
@PreviewsDayNight
@Composable
internal fun EmptyViewLoadingPreview() = ElementPreview {
EmptyView(delayBeforeShowingContent = 1.seconds)
}