Timeline UI | MessageShield Support

This commit is contained in:
Valere 2024-07-24 15:31:49 +02:00
parent 5129642d68
commit 33e09edd62
10 changed files with 332 additions and 11 deletions

View file

@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.matrix.ui.messages.reply.aProfileTimelineDetailsReady import io.element.android.libraries.matrix.ui.messages.reply.aProfileTimelineDetailsReady
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -137,6 +138,7 @@ internal fun aTimelineItemEvent(
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(), timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(),
readReceiptState: TimelineItemReadReceipts = aTimelineItemReadReceipts(), readReceiptState: TimelineItemReadReceipts = aTimelineItemReadReceipts(),
messageShield: MessageShield? = null,
): TimelineItem.Event { ): TimelineItem.Event {
return TimelineItem.Event( return TimelineItem.Event(
id = UUID.randomUUID().toString(), id = UUID.randomUUID().toString(),
@ -159,7 +161,8 @@ internal fun aTimelineItemEvent(
inReplyTo = inReplyTo, inReplyTo = inReplyTo,
debugInfo = debugInfo, debugInfo = debugInfo,
isThreaded = isThreaded, isThreaded = isThreaded,
origin = null origin = null,
messageShield = messageShield,
) )
} }

View file

@ -0,0 +1,25 @@
/*
* 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
*
* https://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 io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
sealed class MessageShieldPosition {
data class InBubble(val messageShield: MessageShield) : MessageShieldPosition()
data class OutOfBubble(val messageShield: MessageShield) : MessageShieldPosition()
object None : MessageShieldPosition()
}

View file

@ -0,0 +1,120 @@
/*
* 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
*
* https://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.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.messageFromMeBackground
import io.element.android.libraries.designsystem.theme.messageFromOtherBackground
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.api.timeline.item.event.ShieldColor
@Composable
internal fun MessageShieldView(
isMine: Boolean = false,
shield: MessageShield,
modifier: Modifier = Modifier
) {
val borderColor = if (shield.color == ShieldColor.RED) ElementTheme.colors.borderCriticalPrimary else ElementTheme.colors.bgSubtlePrimary
val iconColor = if (shield.color == ShieldColor.RED) ElementTheme.colors.iconCriticalPrimary else ElementTheme.colors.iconSecondary
val backgroundBubbleColor = when {
isMine -> ElementTheme.colors.messageFromMeBackground
else -> ElementTheme.colors.messageFromOtherBackground
}
Row(
verticalAlignment = Alignment.Top,
modifier = modifier
.background(backgroundBubbleColor, RoundedCornerShape(8.dp))
.border(1.dp, borderColor, RoundedCornerShape(8.dp))
.padding(8.dp)
) {
Icon(
imageVector = shield.toIcon(),
contentDescription = null,
modifier = Modifier.size(15.dp),
tint = iconColor,
)
Spacer(modifier = Modifier.size(4.dp))
val textColor = if (shield.color == ShieldColor.RED) ElementTheme.colors.textCriticalPrimary else ElementTheme.colors.textSecondary
Text(
text = shield.message,
style = ElementTheme.typography.fontBodyXsRegular,
color = textColor
)
}
}
@Composable
private fun MessageShield.toIcon(): ImageVector {
return when (this.color) {
ShieldColor.RED -> CompoundIcons.Error()
ShieldColor.GREY -> CompoundIcons.InfoSolid()
}
}
@PreviewsDayNight
@Composable
internal fun MessageShieldViewPreviews() {
ElementPreview {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
MessageShieldView(
shield = MessageShield(
message = "The authenticity of this encrypted message can't be guaranteed on this device.",
color = ShieldColor.GREY
)
)
MessageShieldView(
isMine = true,
shield = MessageShield(
message = "The authenticity of this encrypted message can't be guaranteed on this device.",
color = ShieldColor.GREY
)
)
MessageShieldView(
shield = MessageShield(
message = "Encrypted by a device not verified by its owner.",
color = ShieldColor.RED
)
)
MessageShieldView(
shield = MessageShield(
message = "Encrypted by an unknown or deleted device.",
color = ShieldColor.RED
)
)
}
}
}

View file

@ -17,6 +17,7 @@
package io.element.android.features.messages.impl.timeline.components package io.element.android.features.messages.impl.timeline.components
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.res.Configuration
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
@ -50,6 +51,7 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.invisibleToUser import androidx.compose.ui.semantics.invisibleToUser
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -75,6 +77,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
import io.element.android.features.messages.impl.timeline.model.event.aGreyShield
import io.element.android.features.messages.impl.timeline.model.event.aRedShield
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
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
@ -277,6 +281,7 @@ private fun TimelineItemEventRowContent(
val ( val (
sender, sender,
message, message,
shield,
reactions, reactions,
) = createRefs() ) = createRefs()
@ -328,6 +333,29 @@ private fun TimelineItemEventRowContent(
) )
} }
val shieldPosition = event.shieldPosition()
if (shieldPosition is MessageShieldPosition.OutOfBubble) {
MessageShieldView(
isMine = event.isMine,
shield = shieldPosition.messageShield,
modifier = Modifier
.constrainAs(shield) {
top.linkTo(message.bottom, margin = (-4).dp)
linkStartOrEnd(event)
}
.padding(
// Note: due to the applied constraints, start is left for other's message and right for mine
// In design we want a offset of 6.dp compare to the bubble, so start is 22.dp (16 + 6)
start = when {
event.isMine -> 22.dp
timelineRoomInfo.isDm -> 22.dp
else -> 22.dp + BUBBLE_INCOMING_OFFSET
},
end = 16.dp
),
)
}
// Reactions // Reactions
if (event.reactionsState.reactions.isNotEmpty()) { if (event.reactionsState.reactions.isNotEmpty()) {
TimelineItemReactionsView( TimelineItemReactionsView(
@ -339,7 +367,11 @@ private fun TimelineItemEventRowContent(
onMoreReactionsClick = { onMoreReactionsClick(event) }, onMoreReactionsClick = { onMoreReactionsClick(event) },
modifier = Modifier modifier = Modifier
.constrainAs(reactions) { .constrainAs(reactions) {
top.linkTo(message.bottom, margin = (-4).dp) if (shieldPosition is MessageShieldPosition.OutOfBubble) {
top.linkTo(shield.bottom, margin = (-4).dp)
} else {
top.linkTo(message.bottom, margin = (-4).dp)
}
linkStartOrEnd(event) linkStartOrEnd(event)
} }
.zIndex(1f) .zIndex(1f)
@ -472,6 +504,7 @@ private fun MessageEventBubbleContent(
@Composable @Composable
fun CommonLayout( fun CommonLayout(
timestampPosition: TimestampPosition, timestampPosition: TimestampPosition,
messageShieldPosition: MessageShieldPosition,
showThreadDecoration: Boolean, showThreadDecoration: Boolean,
inReplyToDetails: InReplyToDetails?, inReplyToDetails: InReplyToDetails?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -510,13 +543,30 @@ private fun MessageEventBubbleContent(
canShrinkContent = canShrinkContent, canShrinkContent = canShrinkContent,
modifier = timestampLayoutModifier, modifier = timestampLayoutModifier,
) { onContentLayoutChange -> ) { onContentLayoutChange ->
TimelineItemEventContentView(
content = event.content, if (messageShieldPosition is MessageShieldPosition.InBubble) {
onLinkClick = onLinkClick, Column {
eventSink = eventSink, TimelineItemEventContentView(
onContentLayoutChange = onContentLayoutChange, content = event.content,
modifier = contentModifier onLinkClick = onLinkClick,
) eventSink = eventSink,
onContentLayoutChange = onContentLayoutChange,
modifier = contentModifier
)
MessageShieldView(
modifier = Modifier.padding(start = 8.dp, end = 8.dp),
shield = messageShieldPosition.messageShield,
)
}
} else {
TimelineItemEventContentView(
content = event.content,
onLinkClick = onLinkClick,
eventSink = eventSink,
onContentLayoutChange = onContentLayoutChange,
modifier = contentModifier
)
}
} }
} }
val inReplyTo = @Composable { inReplyTo: InReplyToDetails -> val inReplyTo = @Composable { inReplyTo: InReplyToDetails ->
@ -551,9 +601,11 @@ private fun MessageEventBubbleContent(
is TimelineItemPollContent -> TimestampPosition.Below is TimelineItemPollContent -> TimestampPosition.Below
else -> TimestampPosition.Default else -> TimestampPosition.Default
} }
val messageShieldPosition = event.shieldPosition()
CommonLayout( CommonLayout(
showThreadDecoration = event.isThreaded, showThreadDecoration = event.isThreaded,
timestampPosition = timestampPosition, messageShieldPosition = messageShieldPosition,
timestampPosition = if (messageShieldPosition is MessageShieldPosition.InBubble) TimestampPosition.Below else timestampPosition,
inReplyToDetails = event.inReplyTo, inReplyToDetails = event.inReplyTo,
canShrinkContent = event.content is TimelineItemVoiceContent, canShrinkContent = event.content is TimelineItemVoiceContent,
modifier = bubbleModifier.semantics(mergeDescendants = true) { modifier = bubbleModifier.semantics(mergeDescendants = true) {
@ -590,3 +642,68 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
} }
} }
} }
@Preview(
name = "Encryption Shields"
)
@Preview(
name = "Encryption Shields - Night",
uiMode = Configuration.UI_MODE_NIGHT_YES
)
@Composable
internal fun TimelineItemEventRowShieldsPreview() = ElementPreview {
Column {
ATimelineItemEventRow(
event = aTimelineItemEvent(
senderDisplayName = "Sender with a super long name that should ellipsize",
isMine = true,
content = aTimelineItemTextContent(
body = "Message sent from unsigned device"
),
groupPosition = TimelineItemGroupPosition.First,
messageShield = aRedShield()
),
)
ATimelineItemEventRow(
event = aTimelineItemEvent(
senderDisplayName = "Sender with a super long name that should ellipsize",
content = aTimelineItemTextContent(
body = "Short Message with authenticity warning"
),
groupPosition = TimelineItemGroupPosition.Middle,
messageShield = aGreyShield()
),
)
ATimelineItemEventRow(
event = aTimelineItemEvent(
isMine = true,
content = aTimelineItemImageContent().copy(
aspectRatio = 2.5f
),
groupPosition = TimelineItemGroupPosition.Last,
messageShield = aRedShield()
),
)
ATimelineItemEventRow(
event = aTimelineItemEvent(
content = aTimelineItemImageContent().copy(
aspectRatio = 2.5f
),
groupPosition = TimelineItemGroupPosition.Last,
messageShield = aGreyShield()
),
)
}
}
private fun TimelineItem.Event.shieldPosition(): MessageShieldPosition {
if (this.messageShield == null) return MessageShieldPosition.None
return when (this.content) {
is TimelineItemImageContent,
is TimelineItemVideoContent,
is TimelineItemStickerContent,
is TimelineItemLocationContent,
is TimelineItemPollContent -> MessageShieldPosition.OutOfBubble(this.messageShield)
else -> MessageShieldPosition.InBubble(this.messageShield)
}
}

View file

@ -85,6 +85,7 @@ class TimelineItemEventFactory @Inject constructor(
isThreaded = currentTimelineItem.event.isThreaded(), isThreaded = currentTimelineItem.event.isThreaded(),
debugInfo = currentTimelineItem.event.debugInfo, debugInfo = currentTimelineItem.event.debugInfo,
origin = currentTimelineItem.event.origin, origin = currentTimelineItem.event.origin,
messageShield = currentTimelineItem.event.messageShield,
) )
} }

View file

@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName
@ -82,6 +83,7 @@ sealed interface TimelineItem {
val isThreaded: Boolean, val isThreaded: Boolean,
val debugInfo: TimelineItemDebugInfo, val debugInfo: TimelineItemDebugInfo,
val origin: TimelineItemEventOrigin?, val origin: TimelineItemEventOrigin?,
val messageShield: MessageShield?,
) : TimelineItem { ) : TimelineItem {
val showSenderInformation = groupPosition.isNew() && !isMine val showSenderInformation = groupPosition.isNew() && !isMine

View file

@ -21,6 +21,8 @@ import android.text.style.StyleSpan
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.core.text.buildSpannedString import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans import androidx.core.text.inSpans
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.api.timeline.item.event.ShieldColor
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
class TimelineItemEventContentProvider : PreviewParameterProvider<TimelineItemEventContent> { class TimelineItemEventContentProvider : PreviewParameterProvider<TimelineItemEventContent> {
@ -102,3 +104,13 @@ fun aTimelineItemStateEventContent(
) = TimelineItemStateEventContent( ) = TimelineItemStateEventContent(
body = body, body = body,
) )
fun aGreyShield() = MessageShield(
message = "The authenticity of this encrypted message can't be guaranteed on this device.",
color = ShieldColor.GREY
)
fun aRedShield() = MessageShield(
message = "Encrypted by a device not verified by its owner.",
color = ShieldColor.RED
)

View file

@ -38,6 +38,7 @@ data class EventTimelineItem(
val content: EventContent, val content: EventContent,
val debugInfo: TimelineItemDebugInfo, val debugInfo: TimelineItemDebugInfo,
val origin: TimelineItemEventOrigin?, val origin: TimelineItemEventOrigin?,
val messageShield: MessageShield?,
) { ) {
fun inReplyTo(): InReplyTo? { fun inReplyTo(): InReplyTo? {
return (content as? MessageContent)?.inReplyTo return (content as? MessageContent)?.inReplyTo

View file

@ -0,0 +1,27 @@
/*
* 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
*
* https://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.libraries.matrix.api.timeline.item.event
data class MessageShield(
val message: String,
val color: ShieldColor,
)
enum class ShieldColor {
RED,
GREY
}

View file

@ -23,14 +23,17 @@ import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugIn
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
import io.element.android.libraries.matrix.api.timeline.item.event.ShieldColor
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import org.matrix.rustcomponents.sdk.Reaction import org.matrix.rustcomponents.sdk.Reaction
import org.matrix.rustcomponents.sdk.ShieldState
import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState
import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo
@ -55,7 +58,8 @@ class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMap
timestamp = it.timestamp().toLong(), timestamp = it.timestamp().toLong(),
content = contentMapper.map(it.content()), content = contentMapper.map(it.content()),
debugInfo = it.debugInfo().map(), debugInfo = it.debugInfo().map(),
origin = it.origin()?.map() origin = it.origin()?.map(),
messageShield = it.getShield(false)?.map(),
) )
} }
} }
@ -128,3 +132,12 @@ private fun RustEventItemOrigin.map(): TimelineItemEventOrigin {
RustEventItemOrigin.PAGINATION -> TimelineItemEventOrigin.PAGINATION RustEventItemOrigin.PAGINATION -> TimelineItemEventOrigin.PAGINATION
} }
} }
private fun ShieldState?.map(): MessageShield? {
return when (this) {
is ShieldState.Grey -> MessageShield(message = this.message, color = ShieldColor.GREY)
is ShieldState.Red -> MessageShield(message = this.message, color = ShieldColor.RED)
ShieldState.None,
null -> null
}
}