From 7a751abdb38798453619bfd597dc91aa2e26a140 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 27 Nov 2025 12:22:57 +0100 Subject: [PATCH] Add indicators in room list for sending event and errors. --- .../home/impl/components/RoomSummaryRow.kt | 94 ++++++++++++++----- .../datasource/RoomListRoomSummaryFactory.kt | 28 +++++- .../features/home/impl/model/LatestEvent.kt | 34 +++++++ .../home/impl/model/RoomListRoomSummary.kt | 2 +- .../impl/model/RoomListRoomSummaryProvider.kt | 28 +++--- .../impl/roomlist/RoomListStateProvider.kt | 7 +- .../impl/model/RoomListBaseRoomSummaryTest.kt | 2 +- .../home/impl/roomlist/RoomListViewTest.kt | 6 +- .../src/main/res/values/localazy.xml | 1 + 9 files changed, 160 insertions(+), 42 deletions(-) create mode 100644 features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/LatestEvent.kt diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt index c43f7dcdd2..da0f47ba05 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/components/RoomSummaryRow.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.zIndex import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.home.impl.R +import io.element.android.features.home.impl.model.LatestEvent import io.element.android.features.home.impl.model.RoomListRoomSummary import io.element.android.features.home.impl.model.RoomListRoomSummaryProvider import io.element.android.features.home.impl.model.RoomSummaryDisplayType @@ -120,6 +121,7 @@ internal fun RoomSummaryRow( ) { NameAndTimestampRow( name = room.name, + latestEvent = room.latestEvent, timestamp = room.timestamp, isHighlighted = room.isHighlighted ) @@ -136,6 +138,7 @@ internal fun RoomSummaryRow( ) { NameAndTimestampRow( name = room.name, + latestEvent = room.latestEvent, timestamp = null, isHighlighted = room.isHighlighted ) @@ -211,6 +214,7 @@ private fun RoomSummaryScaffoldRow( @Composable private fun NameAndTimestampRow( name: String?, + latestEvent: LatestEvent, timestamp: String?, isHighlighted: Boolean, modifier: Modifier = Modifier @@ -219,16 +223,42 @@ private fun NameAndTimestampRow( modifier = modifier.fillMaxWidth(), horizontalArrangement = spacedBy(16.dp) ) { - // Name - Text( + Row( modifier = Modifier.weight(1f), - style = ElementTheme.typography.fontBodyLgMedium, - text = name ?: stringResource(id = CommonStrings.common_no_room_name), - fontStyle = FontStyle.Italic.takeIf { name == null }, - color = ElementTheme.colors.roomListRoomName, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + verticalAlignment = Alignment.CenterVertically, + ) { + // Name + Text( + style = ElementTheme.typography.fontBodyLgMedium, + text = name ?: stringResource(id = CommonStrings.common_no_room_name), + fontStyle = FontStyle.Italic.takeIf { name == null }, + color = ElementTheme.colors.roomListRoomName, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + // Picto + when (latestEvent) { + is LatestEvent.Sending -> { + Spacer(modifier = Modifier.width(4.dp)) + Icon( + modifier = Modifier.size(16.dp), + imageVector = CompoundIcons.Time(), + contentDescription = null, + tint = ElementTheme.colors.iconTertiary, + ) + } + is LatestEvent.Error -> { + Spacer(modifier = Modifier.width(4.dp)) + Icon( + modifier = Modifier.size(16.dp), + imageVector = CompoundIcons.ErrorSolid(), + contentDescription = null, + tint = ElementTheme.colors.iconCriticalPrimary, + ) + } + else -> Unit + } + } // Timestamp Text( text = timestamp ?: "", @@ -274,21 +304,41 @@ private fun MessagePreviewAndIndicatorRow( modifier = modifier.fillMaxWidth(), horizontalArrangement = spacedBy(28.dp) ) { - val messagePreview = if (room.isTombstoned) { - stringResource(R.string.screen_roomlist_tombstoned_room_description) + if (room.isTombstoned) { + Text( + modifier = Modifier.weight(1f), + text = stringResource(R.string.screen_roomlist_tombstoned_room_description), + color = ElementTheme.colors.roomListRoomMessage, + style = ElementTheme.typography.fontBodyMdRegular, + minLines = 2, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) } else { - room.latestEvent.orEmpty() + if (room.latestEvent is LatestEvent.Error) { + Text( + modifier = Modifier.weight(1f), + text = stringResource(CommonStrings.common_message_failed_to_send), + color = ElementTheme.colors.textCriticalPrimary, + style = ElementTheme.typography.fontBodyMdRegular, + minLines = 2, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } else { + val messagePreview = room.latestEvent.content() + val annotatedMessagePreview = messagePreview as? AnnotatedString ?: AnnotatedString(text = messagePreview.orEmpty().toString()) + Text( + modifier = Modifier.weight(1f), + text = annotatedMessagePreview, + color = ElementTheme.colors.roomListRoomMessage, + style = ElementTheme.typography.fontBodyMdRegular, + minLines = 2, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } } - val annotatedMessagePreview = messagePreview as? AnnotatedString ?: AnnotatedString(text = messagePreview.toString()) - Text( - modifier = Modifier.weight(1f), - text = annotatedMessagePreview, - color = ElementTheme.colors.roomListRoomMessage, - style = ElementTheme.typography.fontBodyMdRegular, - minLines = 2, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) // Call and unread Row( diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt index 935410e572..14121516d6 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/datasource/RoomListRoomSummaryFactory.kt @@ -9,6 +9,7 @@ package io.element.android.features.home.impl.datasource import dev.zacsweers.metro.Inject +import io.element.android.features.home.impl.model.LatestEvent import io.element.android.features.home.impl.model.RoomListRoomSummary import io.element.android.features.home.impl.model.RoomSummaryDisplayType import io.element.android.libraries.core.extensions.orEmpty @@ -18,6 +19,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.isDm +import io.element.android.libraries.matrix.api.roomlist.LatestEventValue import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.ui.model.getAvatarData import io.element.android.libraries.matrix.ui.model.toInviteSender @@ -44,7 +46,7 @@ class RoomListRoomSummaryFactory( mode = DateFormatterMode.TimeOrDate, useRelative = true, ), - latestEvent = roomLatestEventFormatter.format(roomSummary.latestEvent, roomInfo.isDm).orEmpty(), + latestEvent = computeLatestEvent(roomSummary.latestEvent, roomInfo.isDm), avatarData = avatarData, userDefinedNotificationMode = roomInfo.userDefinedNotificationMode, hasRoomCall = roomInfo.hasRoomCall, @@ -71,4 +73,28 @@ class RoomListRoomSummaryFactory( isSpace = roomInfo.isSpace, ) } + + private fun computeLatestEvent(latestEvent: LatestEventValue, dm: Boolean): LatestEvent { + return when (latestEvent) { + is LatestEventValue.None -> { + LatestEvent.None + } + is LatestEventValue.Local -> { + if (latestEvent.isSending) { + val content = roomLatestEventFormatter.format(latestEvent, dm).orEmpty() + LatestEvent.Sending( + content = content, + ) + } else { + LatestEvent.Error + } + } + is LatestEventValue.Remote -> { + val content = roomLatestEventFormatter.format(latestEvent, dm).orEmpty() + LatestEvent.Regular( + content = content, + ) + } + } + } } diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/LatestEvent.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/LatestEvent.kt new file mode 100644 index 0000000000..6a268e8284 --- /dev/null +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/LatestEvent.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.home.impl.model + +import androidx.compose.runtime.Immutable + +@Immutable +sealed interface LatestEvent { + data object None : LatestEvent + + data class Regular( + val content: CharSequence?, + ) : LatestEvent + + data class Sending( + val content: CharSequence?, + ) : LatestEvent + + data object Error : LatestEvent + + fun content(): CharSequence? { + return when (this) { + is None -> null + is Regular -> content + is Sending -> content + is Error -> null + } + } +} diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt index aa983baf56..a59e444455 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummary.kt @@ -29,7 +29,7 @@ data class RoomListRoomSummary( val numberOfUnreadNotifications: Long, val isMarkedUnread: Boolean, val timestamp: String?, - val latestEvent: CharSequence?, + val latestEvent: LatestEvent, val avatarData: AvatarData, val userDefinedNotificationMode: RoomNotificationMode?, val hasRoomCall: Boolean, diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt index e88d6d0972..65c70f98ed 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/model/RoomListRoomSummaryProvider.kt @@ -25,12 +25,14 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider { name = "Room", numberOfUnreadMessages = 1, timestamp = "14:18", - lastMessage = "A very very very very long message which suites on two lines", + latestEvent = LatestEvent.Regular("A very very very very long message which suites on two lines"), avatarData = AvatarData("!id", "R", size = AvatarSize.RoomListItem), id = "!roomId:domain", ), @@ -96,7 +97,7 @@ internal fun aRoomListRoomSummaryList(): ImmutableList { name = "Room#2", numberOfUnreadMessages = 0, timestamp = "14:16", - lastMessage = "A short message", + latestEvent = LatestEvent.Regular("A short message"), avatarData = AvatarData("!id", "Z", size = AvatarSize.RoomListItem), id = "!roomId2:domain", ), @@ -119,7 +120,7 @@ internal fun generateRoomListRoomSummaryList( name = "Room#$index", numberOfUnreadMessages = 0, timestamp = "14:16", - lastMessage = "A message", + latestEvent = LatestEvent.Regular("A message"), avatarData = AvatarData("!id$index", "${(65 + index % 26).toChar()}", size = AvatarSize.RoomListItem), id = "!roomId$index:domain", ) diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt index cbf6c145ad..0a5350d086 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/model/RoomListBaseRoomSummaryTest.kt @@ -96,7 +96,7 @@ internal fun createRoomListRoomSummary( numberOfUnreadNotifications = numberOfUnreadNotifications, isMarkedUnread = isMarkedUnread, timestamp = timestamp, - latestEvent = "", + latestEvent = LatestEvent.Regular(""), avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem), displayType = displayType, userDefinedNotificationMode = userDefinedNotificationMode, diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt index 7855180926..bb82d51a79 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/roomlist/RoomListViewTest.kt @@ -170,7 +170,7 @@ class RoomListViewTest { // Remove automatic initial events eventsRecorder.clear() - rule.onNodeWithText(room0.latestEvent!!.toString()).performClick() + rule.onNodeWithText(room0.latestEvent.content().toString()).performClick() } eventsRecorder.assertEmpty() @@ -192,7 +192,7 @@ class RoomListViewTest { ) // Remove automatic initial events eventsRecorder.clear() - rule.onNodeWithText(room0.latestEvent!!.toString()) + rule.onNodeWithText(room0.latestEvent.content().toString()) .performClick() .performClick() } @@ -214,7 +214,7 @@ class RoomListViewTest { // Remove automatic initial events eventsRecorder.clear() - rule.onNodeWithText(room0.latestEvent!!.toString()).performTouchInput { longClick() } + rule.onNodeWithText(room0.latestEvent.content().toString()).performTouchInput { longClick() } eventsRecorder.assertSingle(RoomListEvents.ShowContextMenu(room0)) } diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index b1586a8d66..6bf57481d4 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -246,6 +246,7 @@ Reason: %1$s." "Message" "Message actions" + "Message failed to send" "Message layout" "Message removed" "Modern"