Merge branch 'hotfix/26.09.4'

This commit is contained in:
Jorge Martín
2026-09-29 16:39:58 +02:00
166 changed files with 370 additions and 507 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 // Call and unread
Row( Row(
modifier = Modifier 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 // Used to force this line to be read aloud earlier than the latest event when using Talkback
.zIndex(-1f), .zIndex(-1f),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -358,18 +358,21 @@ private fun MessagePreviewAndIndicatorRow(
isAudio = room.activeCallIntent == CallIntent.AUDIO isAudio = room.activeCallIntent == CallIntent.AUDIO
) )
} }
val isMuted = room.userDefinedNotificationMode == RoomNotificationMode.MUTE if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) {
if (isMuted) {
NotificationOffIndicatorAtom() NotificationOffIndicatorAtom()
} else if (room.numberOfUnreadMentions > 0) { } else if (room.numberOfUnreadMentions > 0) {
MentionIndicatorAtom() MentionIndicatorAtom()
} }
if (room.hasNewContent) { if (room.hasNewContent) {
val contentDescription = stringResource(CommonStrings.a11y_notifications_new_messages) val contentDescription = stringResource(CommonStrings.a11y_notifications_new_messages)
val count = when { val count = if (showUnreadCount) {
showUnreadCount && !isMuted && room.numberOfUnreadNotifications > 0 -> room.numberOfUnreadNotifications if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) {
!showUnreadCount -> 0 room.numberOfUnreadMessages
else -> null } else {
room.numberOfUnreadNotifications
}
} else {
null
} }
UnreadIndicatorAtom( UnreadIndicatorAtom(
color = tint, color = tint,
@@ -454,6 +457,5 @@ internal fun RoomSummaryRowPreview(@PreviewParameter(RoomListRoomSummaryPreviewP
isInviteSeen = data.name == "Bob", isInviteSeen = data.name == "Bob",
onClick = {}, onClick = {},
eventSink = {}, eventSink = {},
showUnreadCount = data.numberOfUnreadNotifications > 0,
) )
} }
@@ -82,24 +82,6 @@ open class RoomListRoomSummaryPreviewParam : PreviewParameterProvider<RoomListRo
numberOfUnreadMentions = 1, numberOfUnreadMentions = 1,
hasRoomCall = hasCall, 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()
}.flatten(), }.flatten(),
@@ -8,43 +8,39 @@
package io.element.android.libraries.designsystem.atomic.atoms 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.Arrangement.spacedBy
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer 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 androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme 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.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toDp import io.element.android.libraries.designsystem.text.toDp
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
private const val MAX_COUNT = 99 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. * 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 * 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]. * @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 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 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 contentColor The text color inside the counter. When null, uses [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 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. * @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. * Only used when [containerColor] is null.
*/ */
@@ -54,51 +50,41 @@ fun CounterAtom(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
containerColor: Color? = null, containerColor: Color? = null,
contentColor: Color? = null, contentColor: Color? = null,
contentPadding: PaddingValues = PaddingValues.Zero,
textStyle: TextStyle = CounterAtomDefaults.textStyle, textStyle: TextStyle = CounterAtomDefaults.textStyle,
dotSize: Dp = 12.0.dp,
isCritical: Boolean = false, isCritical: Boolean = false,
) { ) {
if (count < 1) return
val countAsText = when (count) { val countAsText = when (count) {
in Int.MIN_VALUE..0 -> null in 0..MAX_COUNT -> count.toString()
in 1..MAX_COUNT -> count.toString()
else -> MAX_COUNT_STRING else -> MAX_COUNT_STRING
} }
val textMeasurer = rememberTextMeasurer()
val minSingleDigitSize = if (countAsText?.length == 1) { // Measure the maximum count string size
val textMeasurer = rememberTextMeasurer() val textLayoutResult = textMeasurer.measure(
// Measure the maximum count string size text = MAX_COUNT_STRING,
val textLayoutResult = textMeasurer.measure( style = textStyle
text = countAsText,
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,
)
}
}
) )
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),
text = countAsText,
style = textStyle,
color = contentColor ?: ElementTheme.colors.textOnSolidPrimary,
)
}
} }
object CounterAtomDefaults { object CounterAtomDefaults {
@@ -109,7 +95,7 @@ object CounterAtomDefaults {
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun CounterAtomPreview() = ElementPreview { internal fun CounterAtomPreview() = ElementPreview {
Row(horizontalArrangement = spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically) { Column(verticalArrangement = spacedBy(2.dp)) {
CounterAtom(count = 0) CounterAtom(count = 0)
CounterAtom(count = 4) CounterAtom(count = 4)
CounterAtom(count = 99) CounterAtom(count = 99)
@@ -9,16 +9,15 @@
package io.element.android.libraries.designsystem.atomic.atoms package io.element.android.libraries.designsystem.atomic.atoms
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size 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.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
@@ -38,36 +37,35 @@ fun UnreadIndicatorAtom(
isVisible: Boolean = true, isVisible: Boolean = true,
contentDescription: String? = null, contentDescription: String? = null,
border: BorderStroke? = null, border: BorderStroke? = null,
contentPadding: PaddingValues = PaddingValues.Zero,
) { ) {
when { when {
!isVisible -> Spacer(modifier = modifier.size(size)) !isVisible -> Spacer(modifier = modifier.size(size))
else -> count?.let { count != null && count >= 1 -> CounterAtom(
CounterAtom( count = count.toInt(),
count = count.toInt(), modifier = modifier
modifier = modifier .semantics {
.semantics { contentDescription?.let { this.contentDescription = it }
contentDescription?.let { this.contentDescription = it } }
} .then(if (border != null) Modifier.border(border, CircleShape) else Modifier),
.then(if (border != null) Modifier.border(border, RoundedCornerShape(percent = 50)) else Modifier), containerColor = color,
containerColor = color, contentColor = ElementTheme.colors.bgCanvasDefault,
contentColor = ElementTheme.colors.bgCanvasDefault, textStyle = ElementTheme.typography.fontBodySmMedium,
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 @PreviewsDayNight
@Composable @Composable
internal fun UnreadIndicatorAtomPreview() = ElementPreview { internal fun UnreadIndicatorAtomPreview() = ElementPreview {
Row(horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically) { UnreadIndicatorAtom()
UnreadIndicatorAtom(count = null)
UnreadIndicatorAtom(count = 0)
UnreadIndicatorAtom(count = 1)
UnreadIndicatorAtom(count = 10)
UnreadIndicatorAtom(count = 99)
UnreadIndicatorAtom(count = 999)
}
} }
@@ -8,18 +8,17 @@
package io.element.android.libraries.designsystem.theme.components package io.element.android.libraries.designsystem.theme.components
import androidx.compose.animation.core.FiniteAnimationSpec 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.PaddingValues
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FloatingToolbarColors import androidx.compose.material3.FloatingToolbarColors
import androidx.compose.material3.FloatingToolbarDefaults import androidx.compose.material3.FloatingToolbarDefaults
import androidx.compose.material3.FloatingToolbarHorizontalFabPosition import androidx.compose.material3.FloatingToolbarHorizontalFabPosition
@@ -29,6 +28,7 @@ import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipDefaults import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberTooltipState import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
@@ -139,20 +139,8 @@ fun HorizontalFloatingToolbarItem(
contentColor = ElementTheme.colors.iconSecondary, contentColor = ElementTheme.colors.iconSecondary,
) )
} }
BadgedBox( Box {
badge = { FilledIconButton(
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), modifier = Modifier.widthIn(min = 56.dp),
colors = colors, colors = colors,
onClick = onClick, onClick = onClick,
@@ -163,6 +151,15 @@ fun HorizontalFloatingToolbarItem(
contentDescription = tooltipLabel, 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,47 +172,18 @@ fun HorizontalFloatingToolbarSeparator(modifier: Modifier = Modifier) {
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun HorizontalFloatingToolbarPreview() = ElementPreview { internal fun HorizontalFloatingToolbarPreview() = ElementPreview {
Column { ContentToPreview(
ContentToPreview( floatingActionButton = {
count = 1, FloatingActionButton(
floatingActionButton = { onClick = {},
FloatingActionButton( ) {
onClick = {}, Icon(
) { imageVector = CompoundIcons.Plus(),
Icon( contentDescription = null,
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 @PreviewsDayNight
@@ -229,11 +197,10 @@ internal fun HorizontalFloatingToolbarNoFabPreview() = ElementPreview {
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
private fun ContentToPreview( private fun ContentToPreview(
count: Int? = 6,
floatingActionButton: (@Composable () -> Unit)?, floatingActionButton: (@Composable () -> Unit)?,
) { ) {
HorizontalFloatingToolbar( HorizontalFloatingToolbar(
modifier = Modifier.padding(horizontal = 28.dp, vertical = 16.dp), modifier = Modifier.padding(28.dp),
floatingActionButton = floatingActionButton, floatingActionButton = floatingActionButton,
) { ) {
listOf( listOf(
@@ -247,7 +214,7 @@ private fun ContentToPreview(
icon = icon, icon = icon,
tooltipLabel = "Label", tooltipLabel = "Label",
isSelected = index == 0, isSelected = index == 0,
counter = if (index == 0) count else null, counter = if (index == 0) 6 else null,
forceRenderingTooltip = true, forceRenderingTooltip = true,
onClick = { }, onClick = { },
) )
@@ -9,7 +9,6 @@
package io.element.android.libraries.matrix.ui.model package io.element.android.libraries.matrix.ui.model
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle 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.api.room.RoomMember
import io.element.android.libraries.matrix.ui.R import io.element.android.libraries.matrix.ui.R
@Immutable
data class InviteSender( data class InviteSender(
val userId: UserId, val userId: UserId,
val displayName: String, 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]. * Release number in the month. Value must be in [0,99].
* Do not update this value. it is updated by the release script. * Do not update this value. it is updated by the release script.
*/ */
private const val versionReleaseNumber = 3 private const val versionReleaseNumber = 4
object Versions { object Versions {
/** /**

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