Make verification screens scrollable and emoji labels multiline (#4449)

* Make self verification screens scrollable

* Remove unused fields from `VerificationEmoji`

* Make only the header and content scroll in `HeaderFooterPage`.

* Use the right 'emoji' icon in both flows (`ReactionSolid`)

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa
2025-03-21 12:18:38 +01:00
committed by GitHub
parent 93d27735e4
commit d27a61a588
47 changed files with 213 additions and 144 deletions

View File

@@ -85,7 +85,7 @@ fun JoinRoomView(
) {
HeaderFooterPage(
containerColor = Color.Transparent,
paddingValues = PaddingValues(
contentPadding = PaddingValues(
horizontal = 16.dp,
vertical = 32.dp
),

View File

@@ -51,7 +51,7 @@ fun RoomAliasResolverView(
) {
HeaderFooterPage(
containerColor = Color.Transparent,
paddingValues = PaddingValues(
contentPadding = PaddingValues(
horizontal = 16.dp,
vertical = 32.dp
),

View File

@@ -13,9 +13,11 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
@@ -35,6 +37,7 @@ 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.Button
import io.element.android.libraries.designsystem.theme.components.InvisibleButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar
@@ -67,7 +70,8 @@ fun IncomingVerificationView(
step is Step.Completed -> Unit
else -> BackButton(onClick = { state.eventSink(IncomingVerificationViewEvents.GoBack) })
}
}
},
colors = topAppBarColors(containerColor = Color.Transparent),
)
},
header = {
@@ -77,7 +81,8 @@ fun IncomingVerificationView(
IncomingVerificationBottomMenu(
state = state,
)
}
},
isScrollable = true,
) {
IncomingVerificationContent(
step = step,
@@ -222,7 +227,11 @@ private fun IncomingVerificationBottomMenu(
}
is Step.Verifying -> {
if (step.isWaiting) {
// Show nothing
// Add invisible buttons to keep the same screen layout
VerificationBottomMenu {
InvisibleButton()
InvisibleButton()
}
} else {
VerificationBottomMenu {
Button(

View File

@@ -16,9 +16,11 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@@ -91,7 +93,8 @@ fun VerifySelfSessionView(
{ BackButton(onClick = ::cancelOrResetFlow) }
} else {
{}
}
},
colors = topAppBarColors(containerColor = Color.Transparent)
)
},
header = {
@@ -103,7 +106,8 @@ fun VerifySelfSessionView(
onCancelClick = ::cancelOrResetFlow,
onContinueClick = onFinish,
)
}
},
isScrollable = true,
) {
VerifySelfSessionContent(
flowState = step,
@@ -124,13 +128,13 @@ private fun VerifySelfSessionHeader(step: Step, request: VerificationRequest.Out
}
Step.AwaitingOtherDeviceResponse -> BigIcon.Style.Loading
Step.Canceled -> BigIcon.Style.AlertSolid
Step.Ready -> BigIcon.Style.Default(CompoundIcons.Reaction())
Step.Ready -> BigIcon.Style.Default(CompoundIcons.ReactionSolid())
Step.Completed -> BigIcon.Style.SuccessSolid
is Step.Verifying -> {
if (step.state is AsyncData.Loading<Unit>) {
BigIcon.Style.Loading
} else {
BigIcon.Style.Default(CompoundIcons.Reaction())
BigIcon.Style.Default(CompoundIcons.ReactionSolid())
}
}
is Step.Exit -> return
@@ -272,7 +276,11 @@ private fun VerifySelfSessionBottomMenu(
is Step.AwaitingOtherDeviceResponse -> Unit
is Step.Verifying -> {
if (isVerifying) {
// Show nothing
// Add invisible buttons to keep the same screen layout
VerificationBottomMenu {
InvisibleButton()
InvisibleButton()
}
} else {
VerificationBottomMenu {
Button(

View File

@@ -23,11 +23,11 @@ internal fun aDecimalsSessionVerificationData(
}
private fun aVerificationEmojiList() = listOf(
VerificationEmoji(number = 27, emoji = "🍕", description = "Pizza"),
VerificationEmoji(number = 54, emoji = "🚀", description = "Rocket"),
VerificationEmoji(number = 54, emoji = "🚀", description = "Rocket"),
VerificationEmoji(number = 42, emoji = "📕", description = "Book"),
VerificationEmoji(number = 48, emoji = "🔨", description = "Hammer"),
VerificationEmoji(number = 48, emoji = "🔨", description = "Hammer"),
VerificationEmoji(number = 63, emoji = "📌", description = "Pin"),
VerificationEmoji(number = 27),
VerificationEmoji(number = 54),
VerificationEmoji(number = 54),
VerificationEmoji(number = 42),
VerificationEmoji(number = 48),
VerificationEmoji(number = 48),
VerificationEmoji(number = 63),
)

View File

@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.runtime.Composable
@@ -38,7 +39,7 @@ internal fun VerificationContentVerifying(
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier.fillMaxSize(),
modifier = modifier.fillMaxSize().padding(bottom = 20.dp),
contentAlignment = Alignment.Center
) {
when (data) {
@@ -86,8 +87,8 @@ private fun EmojiItemView(emoji: VerificationEmoji, modifier: Modifier = Modifie
text = stringResource(id = emojiResource.nameRes),
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
overflow = TextOverflow.Visible,
textAlign = TextAlign.Center,
)
}
}

View File

@@ -185,7 +185,7 @@ class VerifySelfSessionPresenterTest {
@Test
fun `present - When verification is approved, the flow completes if there is no error`() = runTest {
val emojis = listOf(
VerificationEmoji(number = 30, emoji = "😀", description = "Smiley")
VerificationEmoji(number = 30)
)
val service = unverifiedSessionService(
requestSessionVerificationLambda = { },

View File

@@ -10,18 +10,21 @@ package io.element.android.libraries.designsystem.atomic.pages
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.preview.ElementPreview
@@ -31,7 +34,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
/**
* @param modifier Classical modifier.
* @param paddingValues padding values to apply to the content.
* @param contentPadding padding values to apply to the content.
* @param containerColor color of the container. Set to [Color.Transparent] if you provide a background in the [modifier].
* @param isScrollable if the whole content should be scrollable.
* @param background optional background component.
@@ -44,7 +47,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun HeaderFooterPage(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(20.dp),
contentPadding: PaddingValues = PaddingValues(20.dp),
containerColor: Color = ElementTheme.colors.bgCanvasDefault,
isScrollable: Boolean = false,
background: @Composable () -> Unit = {},
@@ -53,64 +56,67 @@ fun HeaderFooterPage(
footer: @Composable () -> Unit = {},
content: @Composable () -> Unit = {},
) {
val topBar = remember { movableContentOf(topBar) }
val header = remember { movableContentOf(header) }
val footer = remember { movableContentOf(footer) }
val content = remember { movableContentOf(content) }
Scaffold(
modifier = modifier,
topBar = topBar,
containerColor = containerColor,
) { padding ->
) { insetsPadding ->
val layoutDirection = LocalLayoutDirection.current
val contentInsetsPadding = remember(insetsPadding, layoutDirection) {
PaddingValues(
start = insetsPadding.calculateStartPadding(layoutDirection),
top = insetsPadding.calculateTopPadding(),
end = insetsPadding.calculateEndPadding(layoutDirection),
)
}
val footerInsetsPadding = remember(insetsPadding, layoutDirection) {
PaddingValues(
start = insetsPadding.calculateStartPadding(layoutDirection),
end = insetsPadding.calculateEndPadding(layoutDirection),
bottom = insetsPadding.calculateBottomPadding(),
)
}
Box {
background()
if (isScrollable) {
// Render in a LazyColumn
LazyColumn(
modifier = Modifier
.padding(paddingValues = paddingValues)
.padding(padding)
.consumeWindowInsets(padding)
.imePadding()
) {
// Header
item {
header()
}
// Content
item {
content()
}
// Footer
item {
Box(modifier = Modifier.padding(horizontal = 16.dp)) {
footer()
}
}
}
} else {
// Render in a Column
// Render in a Column
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues = contentPadding)
.consumeWindowInsets(insetsPadding)
.imePadding(),
) {
// Content
Column(
modifier = Modifier
.padding(paddingValues = paddingValues)
.padding(padding)
.consumeWindowInsets(padding)
.imePadding()
.fillMaxWidth()
.run {
if (isScrollable) {
verticalScroll(rememberScrollState())
} else {
Modifier
}
}
// Apply insets here so if the content is scrollable it can get below the top app bar if needed
.padding(contentInsetsPadding)
.weight(1f),
) {
// Header
header()
// Content
Column(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
) {
Box(modifier = Modifier.weight(1f)) {
content()
}
// Footer
Box(modifier = Modifier.padding(horizontal = 16.dp)) {
footer()
}
}
// Footer
Box(
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth()
.padding(footerInsetsPadding)
) {
footer()
}
}
}
@@ -123,8 +129,7 @@ internal fun HeaderFooterPagePreview() = ElementPreview {
HeaderFooterPage(
content = {
Box(
Modifier
.fillMaxSize(),
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Text(
@@ -159,3 +164,46 @@ internal fun HeaderFooterPagePreview() = ElementPreview {
}
)
}
@PreviewsDayNight
@Composable
internal fun HeaderFooterPageScrollablePreview() = ElementPreview {
HeaderFooterPage(
content = {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Text(
text = "Content",
style = ElementTheme.typography.fontHeadingXlBold
)
}
},
header = {
Box(
Modifier
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Text(
text = "Header",
style = ElementTheme.typography.fontHeadingXlBold
)
}
},
footer = {
Box(
Modifier
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Text(
text = "Footer",
style = ElementTheme.typography.fontHeadingXlBold
)
}
},
isScrollable = true,
)
}

View File

@@ -25,6 +25,4 @@ sealed interface SessionVerificationData {
// https://spec.matrix.org/unstable/client-server-api/#sas-method-emoji
data class VerificationEmoji(
val number: Int,
val emoji: String,
val description: String,
)

View File

@@ -274,8 +274,6 @@ private fun RustSessionVerificationData.map(): SessionVerificationData {
emoji.use { sessionVerificationEmoji ->
VerificationEmoji(
number = sessionVerificationData.indices[index].toInt(),
emoji = sessionVerificationEmoji.symbol(),
description = sessionVerificationEmoji.description(),
)
}
},

View File

@@ -68,6 +68,7 @@ class KonsistPreviewTest {
"DefaultRoomListTopBarWithIndicatorPreview",
"FocusedEventPreview",
"GradientFloatingActionButtonCircleShapePreview",
"HeaderFooterPageScrollablePreview",
"IconsCompoundPreview",
"IconsOtherPreview",
"MarkdownTextComposerEditPreview",