From 73abf548eae9e9e98a4389a11d09901a60e1e688 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 12 Feb 2024 16:25:33 +0100 Subject: [PATCH] Render correctly in reply to data when Event cannot be decrypted or has been redacted --- changelog.d/2318.misc | 1 + .../components/TimelineItemEventRow.kt | 52 ++++++++++++++++--- ...ItemEventRowWithReplyInformativePreview.kt | 42 +++++++++++++++ .../TimelineItemEventRowWithReplyPreview.kt | 9 ++-- .../impl/timeline/model/InReplyToMetadata.kt | 25 ++++++++- 5 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 changelog.d/2318.misc create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyInformativePreview.kt diff --git a/changelog.d/2318.misc b/changelog.d/2318.misc new file mode 100644 index 0000000000..0df42d8b7a --- /dev/null +++ b/changelog.d/2318.misc @@ -0,0 +1 @@ +Render correctly in reply to data when Event cannot be decrypted or has been redacted 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 1494b1fd61..ee8411fee8 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 @@ -53,6 +53,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp @@ -92,6 +93,7 @@ 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 import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.icons.CompoundDrawables import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.swipe.SwipeableActionsState @@ -99,6 +101,7 @@ import io.element.android.libraries.designsystem.swipe.rememberSwipeableActionsS import io.element.android.libraries.designsystem.text.toPx import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkData @@ -648,18 +651,51 @@ private fun ReplyToContent( maxLines = 1, overflow = TextOverflow.Ellipsis, ) - Text( - text = metadata?.text.orEmpty(), - style = ElementTheme.typography.fontBodyMdRegular, - textAlign = TextAlign.Start, - color = ElementTheme.materialColors.secondary, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) + ReplyToContentText(metadata) } } } +@Composable +private fun ReplyToContentText(metadata: InReplyToMetadata?) { + val text = when (metadata) { + InReplyToMetadata.Redacted -> stringResource(id = CommonStrings.common_message_removed) + InReplyToMetadata.UnableToDecrypt -> stringResource(id = CommonStrings.common_waiting_for_decryption_key) + else -> metadata?.text.orEmpty() + } + val iconResourceId = when (metadata) { + InReplyToMetadata.Redacted -> CompoundDrawables.ic_compound_delete + InReplyToMetadata.UnableToDecrypt -> CompoundDrawables.ic_compound_time + else -> null + } + val fontStyle = when (metadata) { + is InReplyToMetadata.Informative -> FontStyle.Italic + else -> FontStyle.Normal + } + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + if (iconResourceId != null) { + Icon( + resourceId = iconResourceId, + tint = MaterialTheme.colorScheme.secondary, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + } + Text( + text = text, + style = ElementTheme.typography.fontBodyMdRegular, + fontStyle = fontStyle, + textAlign = TextAlign.Start, + color = MaterialTheme.colorScheme.secondary, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } +} + @PreviewsDayNight @Composable internal fun TimelineItemEventRowPreview() = ElementPreview { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyInformativePreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyInformativePreview.kt new file mode 100644 index 0000000000..2cd1ba1752 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyInformativePreview.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 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.PreviewsDayNight +import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent +import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent + +@PreviewsDayNight +@Composable +internal fun TimelineItemEventRowWithReplyInformativePreview( + @PreviewParameter(InReplyToDetailsInformativeProvider::class) inReplyToDetails: InReplyToDetails, +) = TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails) + +class InReplyToDetailsInformativeProvider : InReplyToDetailsProvider() { + override val values: Sequence + get() = sequenceOf( + RedactedContent, + UnableToDecryptContent(UnableToDecryptContent.Data.Unknown), + ).map { + aInReplyToDetails( + eventContent = it, + ) + } +} 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 146a985039..4f10e8dc39 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 @@ -53,7 +53,10 @@ import kotlinx.collections.immutable.persistentMapOf @Composable internal fun TimelineItemEventRowWithReplyPreview( @PreviewParameter(InReplyToDetailsProvider::class) inReplyToDetails: InReplyToDetails, -) = ElementPreview { +) = TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails) + +@Composable +internal fun TimelineItemEventRowWithReplyContentToPreview(inReplyToDetails: InReplyToDetails) = ElementPreview { Column { sequenceOf(false, true).forEach { ATimelineItemEventRow( @@ -83,7 +86,7 @@ internal fun TimelineItemEventRowWithReplyPreview( } } -class InReplyToDetailsProvider : PreviewParameterProvider { +open class InReplyToDetailsProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMessageContent( @@ -156,7 +159,7 @@ class InReplyToDetailsProvider : PreviewParameterProvider { type = type, ) - private fun aInReplyToDetails( + protected fun aInReplyToDetails( eventContent: EventContent, ) = InReplyToDetails( eventId = EventId("\$event"), 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 d5803ddd72..4ba96272b2 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 @@ -21,12 +21,20 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.res.stringResource import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent +import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.PollContent +import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent +import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent +import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent +import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent +import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent +import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo @@ -46,6 +54,13 @@ internal sealed interface InReplyToMetadata { data class Text( override val text: String ) : InReplyToMetadata + + abstract class Informative : InReplyToMetadata { + override val text: String? = null + } + + data object Redacted : Informative() + data object UnableToDecrypt : Informative() } /** @@ -112,5 +127,13 @@ internal fun InReplyToDetails.metadata(): InReplyToMetadata? = when (eventConten type = AttachmentThumbnailType.Poll, ) ) - else -> null + is RedactedContent -> InReplyToMetadata.Redacted + is UnableToDecryptContent -> InReplyToMetadata.UnableToDecrypt + is FailedToParseMessageLikeContent, + is FailedToParseStateContent, + is ProfileChangeContent, + is RoomMembershipContent, + is StateContent, + UnknownContent, + null -> null }