Merge branch 'hotfix/26.09.4' into develop

This commit is contained in:
Jorge Martín
2026-09-29 18:31:14 +02:00
162 changed files with 362 additions and 499 deletions
@@ -0,0 +1,2 @@
Main changes in this version: fix an issue with the room list not displaying all activity from unread messages.
Full changelog: https://github.com/element-hq/element-x-android/releases
@@ -345,7 +345,7 @@ private fun MessagePreviewAndIndicatorRow(
// Call and unread
Row(
modifier = Modifier
.heightIn(min = 16.dp)
.height(16.dp)
// Used to force this line to be read aloud earlier than the latest event when using Talkback
.zIndex(-1f),
horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -358,18 +358,21 @@ private fun MessagePreviewAndIndicatorRow(
isAudio = room.activeCallIntent == CallIntent.AUDIO
)
}
val isMuted = room.userDefinedNotificationMode == RoomNotificationMode.MUTE
if (isMuted) {
if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) {
NotificationOffIndicatorAtom()
} else if (room.numberOfUnreadMentions > 0) {
MentionIndicatorAtom()
}
if (room.hasNewContent) {
val contentDescription = stringResource(CommonStrings.a11y_notifications_new_messages)
val count = when {
showUnreadCount && !isMuted && room.numberOfUnreadNotifications > 0 -> room.numberOfUnreadNotifications
!showUnreadCount -> 0
else -> null
val count = if (showUnreadCount) {
if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) {
room.numberOfUnreadMessages
} else {
room.numberOfUnreadNotifications
}
} else {
null
}
UnreadIndicatorAtom(
color = tint,
@@ -454,6 +457,5 @@ internal fun RoomSummaryRowPreview(@PreviewParameter(RoomListRoomSummaryPreviewP
isInviteSeen = data.name == "Bob",
onClick = {},
eventSink = {},
showUnreadCount = data.numberOfUnreadNotifications > 0,
)
}
@@ -82,24 +82,6 @@ open class RoomListRoomSummaryPreviewParam : PreviewParameterProvider<RoomListRo
numberOfUnreadMentions = 1,
hasRoomCall = hasCall,
),
aRoomListRoomSummary(
name = roomNotificationMode.name,
latestEvent = LatestEvent.Synced("New notifications" + if (hasCall) ", call" else ""),
notificationMode = roomNotificationMode,
numberOfUnreadMessages = 0,
numberOfUnreadMentions = 0,
numberOfUnreadNotifications = 1,
hasRoomCall = hasCall,
),
aRoomListRoomSummary(
name = roomNotificationMode.name,
latestEvent = LatestEvent.Synced("New notifications" + if (hasCall) ", call" else ""),
notificationMode = roomNotificationMode,
numberOfUnreadMessages = 0,
numberOfUnreadMentions = 0,
numberOfUnreadNotifications = 100,
hasRoomCall = hasCall,
),
)
}.flatten()
}.flatten(),
@@ -8,43 +8,39 @@
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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
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.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, just a dot will be displayed instead.
* If the number is less than 1, the counter will not be displayed.
* @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 [SemanticColors.textOnSolidPrimary].
* @param contentPadding The padding to apply to the text inside the counter.
* @param contentColor The text color inside the counter. When null, uses [textOnSolidPrimary].
* @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.
*/
@@ -54,52 +50,42 @@ 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 Int.MIN_VALUE..0 -> null
in 1..MAX_COUNT -> count.toString()
in 0..MAX_COUNT -> count.toString()
else -> MAX_COUNT_STRING
}
val minSingleDigitSize = if (countAsText?.length == 1) {
val textMeasurer = rememberTextMeasurer()
// Measure the maximum count string size
val textLayoutResult = textMeasurer.measure(
text = countAsText,
text = MAX_COUNT_STRING,
style = textStyle
)
val textSize = textLayoutResult.size
maxOf(textSize.width, textSize.height)
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 {
0
ElementTheme.colors.iconAccentPrimary
}
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),
modifier = Modifier.align(Alignment.Center),
text = countAsText,
style = textStyle,
color = contentColor ?: ElementTheme.colors.textOnSolidPrimary,
textAlign = TextAlign.Center,
)
}
}
)
}
object CounterAtomDefaults {
val textStyle: TextStyle
@@ -109,7 +95,7 @@ object CounterAtomDefaults {
@PreviewsDayNight
@Composable
internal fun CounterAtomPreview() = ElementPreview {
Row(horizontalArrangement = spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically) {
Column(verticalArrangement = spacedBy(2.dp)) {
CounterAtom(count = 0)
CounterAtom(count = 4)
CounterAtom(count = 99)
@@ -9,16 +9,15 @@
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.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.shape.CircleShape
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
@@ -38,36 +37,35 @@ fun UnreadIndicatorAtom(
isVisible: Boolean = true,
contentDescription: String? = null,
border: BorderStroke? = null,
contentPadding: PaddingValues = PaddingValues.Zero,
) {
when {
!isVisible -> Spacer(modifier = modifier.size(size))
else -> count?.let {
CounterAtom(
count != null && count >= 1 -> CounterAtom(
count = count.toInt(),
modifier = modifier
.semantics {
contentDescription?.let { this.contentDescription = it }
}
.then(if (border != null) Modifier.border(border, RoundedCornerShape(percent = 50)) else Modifier),
.then(if (border != null) Modifier.border(border, CircleShape) else Modifier),
containerColor = color,
contentColor = ElementTheme.colors.bgCanvasDefault,
textStyle = ElementTheme.typography.fontBodySmMedium,
contentPadding = contentPadding,
)
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),
)
}
}
@PreviewsDayNight
@Composable
internal fun UnreadIndicatorAtomPreview() = ElementPreview {
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)
}
UnreadIndicatorAtom()
}
@@ -8,18 +8,17 @@
package io.element.android.libraries.designsystem.theme.components
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Box
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.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FloatingToolbarColors
import androidx.compose.material3.FloatingToolbarDefaults
import androidx.compose.material3.FloatingToolbarHorizontalFabPosition
@@ -29,6 +28,7 @@ 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
@@ -139,20 +139,8 @@ fun HorizontalFloatingToolbarItem(
contentColor = ElementTheme.colors.iconSecondary,
)
}
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(
Box {
FilledIconButton(
modifier = Modifier.widthIn(min = 56.dp),
colors = colors,
onClick = onClick,
@@ -163,6 +151,15 @@ 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,
)
}
}
}
}
@@ -175,9 +172,7 @@ fun HorizontalFloatingToolbarSeparator(modifier: Modifier = Modifier) {
@PreviewsDayNight
@Composable
internal fun HorizontalFloatingToolbarPreview() = ElementPreview {
Column {
ContentToPreview(
count = 1,
floatingActionButton = {
FloatingActionButton(
onClick = {},
@@ -189,33 +184,6 @@ internal fun HorizontalFloatingToolbarPreview() = ElementPreview {
}
}
)
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
@@ -229,11 +197,10 @@ internal fun HorizontalFloatingToolbarNoFabPreview() = ElementPreview {
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
private fun ContentToPreview(
count: Int? = 6,
floatingActionButton: (@Composable () -> Unit)?,
) {
HorizontalFloatingToolbar(
modifier = Modifier.padding(horizontal = 28.dp, vertical = 16.dp),
modifier = Modifier.padding(28.dp),
floatingActionButton = floatingActionButton,
) {
listOf(
@@ -247,7 +214,7 @@ private fun ContentToPreview(
icon = icon,
tooltipLabel = "Label",
isSelected = index == 0,
counter = if (index == 0) count else null,
counter = if (index == 0) 6 else null,
forceRenderingTooltip = true,
onClick = { },
)
@@ -9,7 +9,6 @@
package io.element.android.libraries.matrix.ui.model
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
@@ -21,7 +20,6 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.ui.R
@Immutable
data class InviteSender(
val userId: UserId,
val displayName: String,
+1 -1
View File
@@ -45,7 +45,7 @@ private const val versionMonth = 9
* 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 = 3
private const val versionReleaseNumber = 4
object Versions {
/**

Some files were not shown because too many files have changed in this diff Show More