Map all replyTo data and add preview for loading and erorr case.
This commit is contained in:
parent
49dd4ad803
commit
b7970b2db8
7 changed files with 138 additions and 29 deletions
|
|
@ -24,7 +24,6 @@ import androidx.compose.foundation.gestures.Orientation
|
||||||
import androidx.compose.foundation.gestures.draggable
|
import androidx.compose.foundation.gestures.draggable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
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.aTimelineItemTextContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo
|
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.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.colors.AvatarColorsProvider
|
||||||
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
import io.element.android.libraries.designsystem.components.EqualWidthColumn
|
||||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||||
|
|
@ -437,7 +437,7 @@ private fun MessageEventBubbleContent(
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalArrangement = spacedBy(4.dp, Alignment.Start),
|
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.Start),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
@ -565,16 +565,30 @@ private fun MessageEventBubbleContent(
|
||||||
}
|
}
|
||||||
val inReplyTo = @Composable { inReplyTo: InReplyToDetails ->
|
val inReplyTo = @Composable { inReplyTo: InReplyToDetails ->
|
||||||
val topPadding = if (showThreadDecoration) 0.dp else 8.dp
|
val topPadding = if (showThreadDecoration) 0.dp else 8.dp
|
||||||
ReplyToContent(
|
val inReplyToModifier = Modifier
|
||||||
senderId = inReplyTo.senderId,
|
.padding(top = topPadding, start = 8.dp, end = 8.dp)
|
||||||
senderProfile = inReplyTo.senderProfile,
|
.clip(RoundedCornerShape(6.dp))
|
||||||
metadata = inReplyTo.metadata(),
|
// FIXME when a node is clickable, its contents won't be added to the semantics tree of its parent
|
||||||
modifier = Modifier
|
// .clickable(enabled = true, onClick = inReplyToClick)
|
||||||
.padding(top = topPadding, start = 8.dp, end = 8.dp)
|
when (inReplyTo) {
|
||||||
.clip(RoundedCornerShape(6.dp))
|
is InReplyToDetails.Ready -> {
|
||||||
// FIXME when a node is clickable, its contents won't be added to the semantics tree of its parent
|
ReplyToContent(
|
||||||
// .clickable(enabled = true, onClick = inReplyToClick)
|
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) {
|
if (inReplyToDetails != null) {
|
||||||
// Use SubComposeLayout only if necessary as it can have consequences on the performance.
|
// Use SubComposeLayout only if necessary as it can have consequences on the performance.
|
||||||
|
|
@ -584,7 +598,7 @@ private fun MessageEventBubbleContent(
|
||||||
contentWithTimestamp()
|
contentWithTimestamp()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Column(modifier = modifier, verticalArrangement = spacedBy(8.dp)) {
|
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
threadDecoration()
|
threadDecoration()
|
||||||
contentWithTimestamp()
|
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
|
@Composable
|
||||||
private fun ReplyToContentText(metadata: InReplyToMetadata?) {
|
private fun ReplyToContentText(metadata: InReplyToMetadata?) {
|
||||||
val text = when (metadata) {
|
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(
|
protected fun aInReplyToDetails(
|
||||||
eventContent: EventContent,
|
eventContent: EventContent,
|
||||||
displayNameAmbiguous: Boolean = false,
|
displayNameAmbiguous: Boolean = false,
|
||||||
) = InReplyToDetails(
|
) = InReplyToDetails.Ready(
|
||||||
eventId = EventId("\$event"),
|
eventId = EventId("\$event"),
|
||||||
eventContent = eventContent,
|
eventContent = eventContent,
|
||||||
senderId = UserId("@Sender:domain"),
|
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.api.timeline.item.event.TextMessageType
|
||||||
import io.element.android.libraries.matrix.ui.messages.toPlainText
|
import io.element.android.libraries.matrix.ui.messages.toPlainText
|
||||||
|
|
||||||
data class InReplyToDetails(
|
sealed class InReplyToDetails(val eventId: EventId) {
|
||||||
val eventId: EventId,
|
class Ready(
|
||||||
val senderId: UserId,
|
eventId: EventId,
|
||||||
val senderProfile: ProfileTimelineDetails,
|
val senderId: UserId,
|
||||||
val eventContent: EventContent?,
|
val senderProfile: ProfileTimelineDetails,
|
||||||
val textContent: String?,
|
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(
|
fun InReplyTo.map(
|
||||||
permalinkParser: PermalinkParser,
|
permalinkParser: PermalinkParser,
|
||||||
) = when (this) {
|
) = when (this) {
|
||||||
is InReplyTo.Ready -> InReplyToDetails(
|
is InReplyTo.Ready -> InReplyToDetails.Ready(
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
senderId = senderId,
|
senderId = senderId,
|
||||||
senderProfile = senderProfile,
|
senderProfile = senderProfile,
|
||||||
|
|
@ -55,5 +60,7 @@ fun InReplyTo.map(
|
||||||
else -> null
|
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.
|
* Metadata can be either a thumbnail with a text OR just a text.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
internal fun InReplyToDetails.metadata(): InReplyToMetadata? = when (eventContent) {
|
internal fun InReplyToDetails.Ready.metadata(): InReplyToMetadata? = when (eventContent) {
|
||||||
is MessageContent -> when (val type = eventContent.type) {
|
is MessageContent -> when (val type = eventContent.type) {
|
||||||
is ImageMessageType -> InReplyToMetadata.Thumbnail(
|
is ImageMessageType -> InReplyToMetadata.Thumbnail(
|
||||||
AttachmentThumbnailInfo(
|
AttachmentThumbnailInfo(
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ sealed interface InReplyTo {
|
||||||
data class NotLoaded(val eventId: EventId) : InReplyTo
|
data class NotLoaded(val eventId: EventId) : InReplyTo
|
||||||
|
|
||||||
/** The event details are pending to be fetched. We should **not** fetch them again. */
|
/** 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. */
|
/** The event details are available. */
|
||||||
data class Ready(
|
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
|
* 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.
|
* 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(),
|
senderProfile = event.senderProfile.map(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is RepliedToEventDetails.Error -> InReplyTo.Error
|
is RepliedToEventDetails.Error -> InReplyTo.Error(
|
||||||
is RepliedToEventDetails.Pending -> InReplyTo.Pending
|
eventId = inReplyToId,
|
||||||
is RepliedToEventDetails.Unavailable -> InReplyTo.NotLoaded(inReplyToId)
|
message = event.message,
|
||||||
|
)
|
||||||
|
RepliedToEventDetails.Pending -> InReplyTo.Pending(
|
||||||
|
eventId = inReplyToId,
|
||||||
|
)
|
||||||
|
is RepliedToEventDetails.Unavailable -> InReplyTo.NotLoaded(
|
||||||
|
eventId = inReplyToId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MessageContent(
|
MessageContent(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue