diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index b5c60bb2af..1b9655c471 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -94,6 +93,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo import io.element.android.features.messages.impl.timeline.model.metadata +import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom import io.element.android.libraries.designsystem.colors.AvatarColorsProvider import io.element.android.libraries.designsystem.components.EqualWidthColumn import io.element.android.libraries.designsystem.components.avatar.Avatar @@ -437,7 +437,7 @@ private fun MessageEventBubbleContent( ) { Row( modifier = modifier, - horizontalArrangement = spacedBy(4.dp, Alignment.Start), + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.Start), verticalAlignment = Alignment.CenterVertically, ) { Icon( @@ -565,16 +565,30 @@ private fun MessageEventBubbleContent( } val inReplyTo = @Composable { inReplyTo: InReplyToDetails -> val topPadding = if (showThreadDecoration) 0.dp else 8.dp - ReplyToContent( - senderId = inReplyTo.senderId, - senderProfile = inReplyTo.senderProfile, - metadata = inReplyTo.metadata(), - modifier = Modifier - .padding(top = topPadding, start = 8.dp, end = 8.dp) - .clip(RoundedCornerShape(6.dp)) - // FIXME when a node is clickable, its contents won't be added to the semantics tree of its parent -// .clickable(enabled = true, onClick = inReplyToClick) - ) + val inReplyToModifier = Modifier + .padding(top = topPadding, start = 8.dp, end = 8.dp) + .clip(RoundedCornerShape(6.dp)) + // FIXME when a node is clickable, its contents won't be added to the semantics tree of its parent + // .clickable(enabled = true, onClick = inReplyToClick) + when (inReplyTo) { + is InReplyToDetails.Ready -> { + ReplyToContent( + senderId = inReplyTo.senderId, + senderProfile = inReplyTo.senderProfile, + metadata = inReplyTo.metadata(), + modifier = inReplyToModifier, + ) + } + is InReplyToDetails.Error -> + ReplyToErrorContent( + data = inReplyTo, + modifier = inReplyToModifier, + ) + is InReplyToDetails.Loading -> + ReplyToLoadingContent( + modifier = inReplyToModifier, + ) + } } if (inReplyToDetails != null) { // Use SubComposeLayout only if necessary as it can have consequences on the performance. @@ -584,7 +598,7 @@ private fun MessageEventBubbleContent( contentWithTimestamp() } } else { - Column(modifier = modifier, verticalArrangement = spacedBy(8.dp)) { + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) { threadDecoration() contentWithTimestamp() } @@ -652,6 +666,44 @@ private fun ReplyToContent( } } +@Composable +private fun ReplyToLoadingContent( + modifier: Modifier = Modifier, +) { + val paddings = PaddingValues(horizontal = 12.dp, vertical = 4.dp) + Row( + modifier + .background(MaterialTheme.colorScheme.surface) + .padding(paddings) + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + PlaceholderAtom(width = 80.dp, height = 12.dp) + PlaceholderAtom(width = 140.dp, height = 14.dp) + } + } +} + +@Composable +private fun ReplyToErrorContent( + data: InReplyToDetails.Error, + modifier: Modifier = Modifier, +) { + val paddings = PaddingValues(horizontal = 12.dp, vertical = 4.dp) + Row( + modifier + .background(MaterialTheme.colorScheme.surface) + .padding(paddings) + ) { + Text( + text = data.message, + style = ElementTheme.typography.fontBodyMdRegular, + color = MaterialTheme.colorScheme.error, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } +} + @Composable private fun ReplyToContentText(metadata: InReplyToMetadata?) { val text = when (metadata) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyOtherPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyOtherPreview.kt new file mode 100644 index 0000000000..2231af606d --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyOtherPreview.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.messages.impl.timeline.model.InReplyToDetails +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.matrix.api.core.EventId + +@PreviewsDayNight +@Composable +internal fun TimelineItemEventRowWithReplyOtherPreview( + @PreviewParameter(InReplyToDetailsOtherProvider::class) inReplyToDetails: InReplyToDetails, +) = ElementPreview { + TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails) +} + +class InReplyToDetailsOtherProvider : InReplyToDetailsProvider() { + override val values: Sequence + get() = sequenceOf( + InReplyToDetails.Loading(eventId = EventId("\$anEventId")), + InReplyToDetails.Error(eventId = EventId("\$anEventId"), message = "An error message."), + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt index 8c59c3c07a..b1500dac2c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt @@ -170,7 +170,7 @@ open class InReplyToDetailsProvider : PreviewParameterProvider protected fun aInReplyToDetails( eventContent: EventContent, displayNameAmbiguous: Boolean = false, - ) = InReplyToDetails( + ) = InReplyToDetails.Ready( eventId = EventId("\$event"), eventContent = eventContent, senderId = UserId("@Sender:domain"), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt index 83627b5b25..41fda8c294 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToDetails.kt @@ -27,18 +27,23 @@ import io.element.android.libraries.matrix.api.timeline.item.event.StickerConten import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.ui.messages.toPlainText -data class InReplyToDetails( - val eventId: EventId, - val senderId: UserId, - val senderProfile: ProfileTimelineDetails, - val eventContent: EventContent?, - val textContent: String?, -) +sealed class InReplyToDetails(val eventId: EventId) { + class Ready( + eventId: EventId, + val senderId: UserId, + val senderProfile: ProfileTimelineDetails, + val eventContent: EventContent?, + val textContent: String?, + ) : InReplyToDetails(eventId) + + class Loading(eventId: EventId) : InReplyToDetails(eventId) + class Error(eventId: EventId, val message: String) : InReplyToDetails(eventId) +} fun InReplyTo.map( permalinkParser: PermalinkParser, ) = when (this) { - is InReplyTo.Ready -> InReplyToDetails( + is InReplyTo.Ready -> InReplyToDetails.Ready( eventId = eventId, senderId = senderId, senderProfile = senderProfile, @@ -55,5 +60,7 @@ fun InReplyTo.map( else -> null } ) - else -> null + is InReplyTo.Error -> InReplyToDetails.Error(eventId, message) + is InReplyTo.NotLoaded -> InReplyToDetails.Loading(eventId) + is InReplyTo.Pending -> InReplyToDetails.Loading(eventId) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToMetadata.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToMetadata.kt index fb63912b69..ee6589f046 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToMetadata.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/InReplyToMetadata.kt @@ -66,7 +66,7 @@ internal sealed interface InReplyToMetadata { * Metadata can be either a thumbnail with a text OR just a text. */ @Composable -internal fun InReplyToDetails.metadata(): InReplyToMetadata? = when (eventContent) { +internal fun InReplyToDetails.Ready.metadata(): InReplyToMetadata? = when (eventContent) { is MessageContent -> when (val type = eventContent.type) { is ImageMessageType -> InReplyToMetadata.Thumbnail( AttachmentThumbnailInfo( diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/InReplyTo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/InReplyTo.kt index 1337491ff8..bdf3c2f6dc 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/InReplyTo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/InReplyTo.kt @@ -26,7 +26,7 @@ sealed interface InReplyTo { data class NotLoaded(val eventId: EventId) : InReplyTo /** The event details are pending to be fetched. We should **not** fetch them again. */ - data object Pending : InReplyTo + data class Pending(val eventId: EventId) : InReplyTo /** The event details are available. */ data class Ready( @@ -44,5 +44,8 @@ sealed interface InReplyTo { * If the reason for the failure is consistent on the server, we'd enter a loop * where we keep trying to fetch the same event. * */ - data object Error : InReplyTo + data class Error( + val eventId: EventId, + val message: String, + ) : InReplyTo } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt index d3a9294cc2..09a66fa656 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt @@ -57,9 +57,16 @@ class EventMessageMapper { senderProfile = event.senderProfile.map(), ) } - is RepliedToEventDetails.Error -> InReplyTo.Error - is RepliedToEventDetails.Pending -> InReplyTo.Pending - is RepliedToEventDetails.Unavailable -> InReplyTo.NotLoaded(inReplyToId) + is RepliedToEventDetails.Error -> InReplyTo.Error( + eventId = inReplyToId, + message = event.message, + ) + RepliedToEventDetails.Pending -> InReplyTo.Pending( + eventId = inReplyToId, + ) + is RepliedToEventDetails.Unavailable -> InReplyTo.NotLoaded( + eventId = inReplyToId + ) } } MessageContent(