Add indicators in room list for sending event and errors.
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -25,12 +25,14 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
||||
aRoomListRoomSummary(displayType = RoomSummaryDisplayType.PLACEHOLDER),
|
||||
aRoomListRoomSummary(),
|
||||
aRoomListRoomSummary(name = null),
|
||||
aRoomListRoomSummary(lastMessage = null),
|
||||
aRoomListRoomSummary(latestEvent = LatestEvent.None),
|
||||
aRoomListRoomSummary(
|
||||
name = "A very long room name that should be truncated",
|
||||
lastMessage = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt" +
|
||||
" ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea com" +
|
||||
"modo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
|
||||
latestEvent = LatestEvent.Regular(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt" +
|
||||
" ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea com" +
|
||||
"modo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."
|
||||
),
|
||||
timestamp = "yesterday",
|
||||
numberOfUnreadMessages = 1,
|
||||
),
|
||||
@@ -44,7 +46,7 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
||||
listOf(
|
||||
aRoomListRoomSummary(
|
||||
name = roomNotificationMode.name,
|
||||
lastMessage = "No activity" + if (hasCall) ", call" else "",
|
||||
latestEvent = LatestEvent.Regular("No activity" + if (hasCall) ", call" else ""),
|
||||
notificationMode = roomNotificationMode,
|
||||
numberOfUnreadMessages = 0,
|
||||
numberOfUnreadMentions = 0,
|
||||
@@ -52,7 +54,7 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
||||
),
|
||||
aRoomListRoomSummary(
|
||||
name = roomNotificationMode.name,
|
||||
lastMessage = "New messages" + if (hasCall) ", call" else "",
|
||||
latestEvent = LatestEvent.Regular("New messages" + if (hasCall) ", call" else ""),
|
||||
notificationMode = roomNotificationMode,
|
||||
numberOfUnreadMessages = 1,
|
||||
numberOfUnreadMentions = 0,
|
||||
@@ -60,7 +62,7 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
||||
),
|
||||
aRoomListRoomSummary(
|
||||
name = roomNotificationMode.name,
|
||||
lastMessage = "New messages, mentions" + if (hasCall) ", call" else "",
|
||||
latestEvent = LatestEvent.Regular("New messages, mentions" + if (hasCall) ", call" else ""),
|
||||
notificationMode = roomNotificationMode,
|
||||
numberOfUnreadMessages = 1,
|
||||
numberOfUnreadMentions = 1,
|
||||
@@ -68,7 +70,7 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
||||
),
|
||||
aRoomListRoomSummary(
|
||||
name = roomNotificationMode.name,
|
||||
lastMessage = "New mentions" + if (hasCall) ", call" else "",
|
||||
latestEvent = LatestEvent.Regular("New mentions" + if (hasCall) ", call" else ""),
|
||||
notificationMode = roomNotificationMode,
|
||||
numberOfUnreadMessages = 0,
|
||||
numberOfUnreadMentions = 1,
|
||||
@@ -127,6 +129,10 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
||||
isTombstoned = true,
|
||||
)
|
||||
),
|
||||
listOf(
|
||||
aRoomListRoomSummary(latestEvent = LatestEvent.Sending("A sending message")),
|
||||
aRoomListRoomSummary(latestEvent = LatestEvent.Error),
|
||||
)
|
||||
).flatten()
|
||||
}
|
||||
|
||||
@@ -148,8 +154,8 @@ internal fun aRoomListRoomSummary(
|
||||
numberOfUnreadMentions: Long = 0,
|
||||
numberOfUnreadNotifications: Long = 0,
|
||||
isMarkedUnread: Boolean = false,
|
||||
lastMessage: String? = "Last message",
|
||||
timestamp: String? = lastMessage?.let { "88:88" },
|
||||
latestEvent: LatestEvent = LatestEvent.Regular("Last message"),
|
||||
timestamp: String? = latestEvent.takeIf { it !is LatestEvent.None }?.let { "88:88" },
|
||||
notificationMode: RoomNotificationMode? = null,
|
||||
hasRoomCall: Boolean = false,
|
||||
avatarData: AvatarData = AvatarData(id, name, size = AvatarSize.RoomListItem),
|
||||
@@ -171,7 +177,7 @@ internal fun aRoomListRoomSummary(
|
||||
numberOfUnreadNotifications = numberOfUnreadNotifications,
|
||||
isMarkedUnread = isMarkedUnread,
|
||||
timestamp = timestamp,
|
||||
latestEvent = lastMessage,
|
||||
latestEvent = latestEvent,
|
||||
avatarData = avatarData,
|
||||
userDefinedNotificationMode = notificationMode,
|
||||
hasRoomCall = hasRoomCall,
|
||||
|
||||
@@ -11,6 +11,7 @@ package io.element.android.features.home.impl.roomlist
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.home.impl.filters.RoomListFiltersState
|
||||
import io.element.android.features.home.impl.filters.aRoomListFiltersState
|
||||
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.features.home.impl.model.aRoomListRoomSummary
|
||||
@@ -88,7 +89,7 @@ internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
|
||||
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<RoomListRoomSummary> {
|
||||
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",
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -246,6 +246,7 @@ Reason: %1$s."</string>
|
||||
</plurals>
|
||||
<string name="common_message">"Message"</string>
|
||||
<string name="common_message_actions">"Message actions"</string>
|
||||
<string name="common_message_failed_to_send">"Message failed to send"</string>
|
||||
<string name="common_message_layout">"Message layout"</string>
|
||||
<string name="common_message_removed">"Message removed"</string>
|
||||
<string name="common_modern">"Modern"</string>
|
||||
|
||||
Reference in New Issue
Block a user