Map all replyTo data and add preview for loading and erorr case.
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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<InReplyToDetails>
|
||||
get() = sequenceOf(
|
||||
InReplyToDetails.Loading(eventId = EventId("\$anEventId")),
|
||||
InReplyToDetails.Error(eventId = EventId("\$anEventId"), message = "An error message."),
|
||||
)
|
||||
}
|
||||
@@ -170,7 +170,7 @@ open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails>
|
||||
protected fun aInReplyToDetails(
|
||||
eventContent: EventContent,
|
||||
displayNameAmbiguous: Boolean = false,
|
||||
) = InReplyToDetails(
|
||||
) = InReplyToDetails.Ready(
|
||||
eventId = EventId("\$event"),
|
||||
eventContent = eventContent,
|
||||
senderId = UserId("@Sender:domain"),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user