mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 16:22:49 +00:00
Allow displaying only unread notifications (#7803)
* Allow displaying only unread notifications Add `FeatureFlags.ShowAllActivityInRoomList` (enabled by default, to match the current behavior) that, when disabled, will display the unread messages of a room instead of unread notifications if no unread notifications are found * Restore UI changes to the badges, `RoomSummaryRow` and `HorizontalFloatingToolbar` * Fix `NavigationBarIcon`: use `BadgeBox` component. * Adjust padding in `RoomSummaryRow` to 6.dp * Fix other indicators not displaying a dot anymore * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
committed by
GitHub
parent
a3ff7e1486
commit
2dd0a47eb4
+50
-36
@@ -8,39 +8,43 @@
|
||||
|
||||
package io.element.android.libraries.designsystem.atomic.atoms
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.coerceAtLeast
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.SemanticColors
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
private const val MAX_COUNT = 99
|
||||
private const val MAX_COUNT_STRING = "+$MAX_COUNT"
|
||||
private const val MAX_COUNT_STRING = "$MAX_COUNT+"
|
||||
|
||||
/**
|
||||
* A counter atom that displays a number in a circle.
|
||||
* Figma link : https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2805-2649&m=dev
|
||||
*
|
||||
* @param count The number to display. If the number is greater than [MAX_COUNT], the counter will display [MAX_COUNT_STRING].
|
||||
* If the number is less than 1, the counter will not be displayed.
|
||||
* If the number is less than 1, just a dot will be displayed instead.
|
||||
* @param modifier The modifier to apply to this layout.
|
||||
* @param containerColor The background color of the counter. When null, uses [isCritical] to pick a default.
|
||||
* @param contentColor The text color inside the counter. When null, uses [textOnSolidPrimary].
|
||||
* @param contentColor The text color inside the counter. When null, uses [SemanticColors.textOnSolidPrimary].
|
||||
* @param contentPadding The padding to apply to the text inside the counter.
|
||||
* @param textStyle The style to apply to the text inside the counter.
|
||||
* @param dotSize The size of the dot when [count] is less than 1. It defaults to `12.0.dp`.
|
||||
* @param isCritical If true, the counter will use a critical color scheme, otherwise it will use an accent color scheme.
|
||||
* Only used when [containerColor] is null.
|
||||
*/
|
||||
@@ -50,41 +54,51 @@ fun CounterAtom(
|
||||
modifier: Modifier = Modifier,
|
||||
containerColor: Color? = null,
|
||||
contentColor: Color? = null,
|
||||
contentPadding: PaddingValues = PaddingValues.Zero,
|
||||
textStyle: TextStyle = CounterAtomDefaults.textStyle,
|
||||
dotSize: Dp = 12.0.dp,
|
||||
isCritical: Boolean = false,
|
||||
) {
|
||||
if (count < 1) return
|
||||
val countAsText = when (count) {
|
||||
in 0..MAX_COUNT -> count.toString()
|
||||
in Int.MIN_VALUE..0 -> null
|
||||
in 1..MAX_COUNT -> count.toString()
|
||||
else -> MAX_COUNT_STRING
|
||||
}
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
// Measure the maximum count string size
|
||||
val textLayoutResult = textMeasurer.measure(
|
||||
text = MAX_COUNT_STRING,
|
||||
style = textStyle
|
||||
)
|
||||
val textSize = textLayoutResult.size
|
||||
val squareSize = maxOf(textSize.width, textSize.height)
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(squareSize.toDp() + 1.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
containerColor ?: if (isCritical) {
|
||||
ElementTheme.colors.iconCriticalPrimary
|
||||
} else {
|
||||
ElementTheme.colors.iconAccentPrimary
|
||||
}
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
|
||||
val minSingleDigitSize = if (countAsText?.length == 1) {
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
// Measure the maximum count string size
|
||||
val textLayoutResult = textMeasurer.measure(
|
||||
text = countAsText,
|
||||
style = textStyle,
|
||||
color = contentColor ?: ElementTheme.colors.textOnSolidPrimary,
|
||||
style = textStyle
|
||||
)
|
||||
val textSize = textLayoutResult.size
|
||||
maxOf(textSize.width, textSize.height)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
androidx.compose.material3.Badge(
|
||||
modifier = modifier.then(
|
||||
if (countAsText == null) Modifier.sizeIn(minWidth = dotSize, minHeight = dotSize) else Modifier
|
||||
),
|
||||
containerColor = containerColor ?: if (isCritical) ElementTheme.colors.iconCriticalPrimary else ElementTheme.colors.iconAccentPrimary,
|
||||
contentColor = contentColor ?: ElementTheme.colors.textOnSolidPrimary,
|
||||
content = countAsText?.let {
|
||||
@Composable {
|
||||
Text(
|
||||
modifier = Modifier.sizeIn(
|
||||
minWidth = (minSingleDigitSize.toDp() - 8.dp).coerceAtLeast(0.dp),
|
||||
minHeight = minSingleDigitSize.toDp()
|
||||
).padding(contentPadding),
|
||||
text = countAsText,
|
||||
style = textStyle,
|
||||
color = contentColor ?: ElementTheme.colors.textOnSolidPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
object CounterAtomDefaults {
|
||||
@@ -95,7 +109,7 @@ object CounterAtomDefaults {
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun CounterAtomPreview() = ElementPreview {
|
||||
Column(verticalArrangement = spacedBy(2.dp)) {
|
||||
Row(horizontalArrangement = spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
CounterAtom(count = 0)
|
||||
CounterAtom(count = 4)
|
||||
CounterAtom(count = 99)
|
||||
|
||||
+28
-26
@@ -9,15 +9,16 @@
|
||||
package io.element.android.libraries.designsystem.atomic.atoms
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
@@ -37,35 +38,36 @@ fun UnreadIndicatorAtom(
|
||||
isVisible: Boolean = true,
|
||||
contentDescription: String? = null,
|
||||
border: BorderStroke? = null,
|
||||
contentPadding: PaddingValues = PaddingValues.Zero,
|
||||
) {
|
||||
when {
|
||||
!isVisible -> Spacer(modifier = modifier.size(size))
|
||||
count != null && count >= 1 -> CounterAtom(
|
||||
count = count.toInt(),
|
||||
modifier = modifier
|
||||
.semantics {
|
||||
contentDescription?.let { this.contentDescription = it }
|
||||
}
|
||||
.then(if (border != null) Modifier.border(border, CircleShape) else Modifier),
|
||||
containerColor = color,
|
||||
contentColor = ElementTheme.colors.bgCanvasDefault,
|
||||
textStyle = ElementTheme.typography.fontBodySmMedium,
|
||||
)
|
||||
else -> Box(
|
||||
modifier = modifier
|
||||
.semantics {
|
||||
contentDescription?.let { this.contentDescription = it }
|
||||
}
|
||||
.size(size)
|
||||
.clip(CircleShape)
|
||||
.background(color)
|
||||
.then(if (border != null) Modifier.border(border, CircleShape) else Modifier),
|
||||
)
|
||||
else -> count?.let {
|
||||
CounterAtom(
|
||||
count = count.toInt(),
|
||||
modifier = modifier
|
||||
.semantics {
|
||||
contentDescription?.let { this.contentDescription = it }
|
||||
}
|
||||
.then(if (border != null) Modifier.border(border, RoundedCornerShape(percent = 50)) else Modifier),
|
||||
containerColor = color,
|
||||
contentColor = ElementTheme.colors.bgCanvasDefault,
|
||||
textStyle = ElementTheme.typography.fontBodySmMedium,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun UnreadIndicatorAtomPreview() = ElementPreview {
|
||||
UnreadIndicatorAtom()
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
UnreadIndicatorAtom(count = null)
|
||||
UnreadIndicatorAtom(count = 0)
|
||||
UnreadIndicatorAtom(count = 1)
|
||||
UnreadIndicatorAtom(count = 10)
|
||||
UnreadIndicatorAtom(count = 99)
|
||||
UnreadIndicatorAtom(count = 999)
|
||||
}
|
||||
}
|
||||
|
||||
+60
-27
@@ -8,16 +8,17 @@
|
||||
package io.element.android.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.FloatingToolbarColors
|
||||
import androidx.compose.material3.FloatingToolbarDefaults
|
||||
import androidx.compose.material3.FloatingToolbarHorizontalFabPosition
|
||||
@@ -27,7 +28,6 @@ import androidx.compose.material3.TooltipAnchorPosition
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.material3.rememberTooltipState
|
||||
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.graphics.Shape
|
||||
@@ -137,8 +137,20 @@ fun HorizontalFloatingToolbarItem(
|
||||
contentColor = ElementTheme.colors.iconSecondary,
|
||||
)
|
||||
}
|
||||
Box {
|
||||
FilledIconButton(
|
||||
BadgedBox(
|
||||
badge = {
|
||||
counter?.let {
|
||||
CounterAtom(
|
||||
count = it,
|
||||
// Tweak the offset so it's centered on the icon
|
||||
modifier = Modifier.offset(x = (-16).dp, y = 10.dp),
|
||||
contentPadding = PaddingValues(vertical = 0.5.dp),
|
||||
textStyle = ElementTheme.typography.fontBodySmMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
IconButton(
|
||||
modifier = Modifier.widthIn(min = 56.dp),
|
||||
colors = colors,
|
||||
onClick = onClick,
|
||||
@@ -149,15 +161,6 @@ fun HorizontalFloatingToolbarItem(
|
||||
contentDescription = tooltipLabel,
|
||||
)
|
||||
}
|
||||
if (counter != null) {
|
||||
CounterAtom(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(top = 6.dp, end = 3.dp),
|
||||
count = counter,
|
||||
textStyle = ElementTheme.typography.fontBodyXsMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,18 +173,47 @@ fun HorizontalFloatingToolbarSeparator(modifier: Modifier = Modifier) {
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun HorizontalFloatingToolbarPreview() = ElementPreview {
|
||||
ContentToPreview(
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Plus(),
|
||||
contentDescription = null,
|
||||
)
|
||||
Column {
|
||||
ContentToPreview(
|
||||
count = 1,
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Plus(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
ContentToPreview(
|
||||
count = 99,
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Plus(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
ContentToPreview(
|
||||
count = 999,
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = {},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.Plus(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@@ -194,10 +226,11 @@ internal fun HorizontalFloatingToolbarNoFabPreview() = ElementPreview {
|
||||
|
||||
@Composable
|
||||
private fun ContentToPreview(
|
||||
count: Int? = 6,
|
||||
floatingActionButton: (@Composable () -> Unit)?,
|
||||
) {
|
||||
HorizontalFloatingToolbar(
|
||||
modifier = Modifier.padding(28.dp),
|
||||
modifier = Modifier.padding(horizontal = 28.dp, vertical = 16.dp),
|
||||
floatingActionButton = floatingActionButton,
|
||||
) {
|
||||
listOf(
|
||||
@@ -211,7 +244,7 @@ private fun ContentToPreview(
|
||||
icon = icon,
|
||||
tooltipLabel = "Label",
|
||||
isSelected = index == 0,
|
||||
counter = if (index == 0) 6 else null,
|
||||
counter = if (index == 0) count else null,
|
||||
forceRenderingTooltip = true,
|
||||
onClick = { },
|
||||
)
|
||||
|
||||
+11
-10
@@ -8,12 +8,10 @@
|
||||
|
||||
package io.element.android.libraries.designsystem.theme.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.CounterAtom
|
||||
|
||||
@@ -24,16 +22,19 @@ fun NavigationBarIcon(
|
||||
count: Int = 0,
|
||||
isCritical: Boolean = false,
|
||||
) {
|
||||
Box(modifier) {
|
||||
BadgedBox(
|
||||
modifier = modifier,
|
||||
badge = {
|
||||
CounterAtom(
|
||||
textStyle = ElementTheme.typography.fontBodyXsMedium,
|
||||
count = count,
|
||||
isCritical = isCritical,
|
||||
)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = imageVector,
|
||||
contentDescription = null,
|
||||
)
|
||||
CounterAtom(
|
||||
modifier = Modifier.offset(11.dp, (-11).dp),
|
||||
textStyle = ElementTheme.typography.fontBodyXsMedium,
|
||||
count = count,
|
||||
isCritical = isCritical,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user