From 33e09edd6268e8ae0a0daadb9905ac5255d897b7 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 24 Jul 2024 15:31:49 +0200 Subject: [PATCH 01/13] Timeline UI | MessageShield Support --- .../impl/timeline/TimelineStateProvider.kt | 5 +- .../components/MessageShieldPosition.kt | 25 ++++ .../timeline/components/MessageShieldView.kt | 120 ++++++++++++++++ .../components/TimelineItemEventRow.kt | 135 ++++++++++++++++-- .../event/TimelineItemEventFactory.kt | 1 + .../impl/timeline/model/TimelineItem.kt | 2 + .../event/TimelineItemEventContentProvider.kt | 12 ++ .../timeline/item/event/EventTimelineItem.kt | 1 + .../api/timeline/item/event/MessageShield.kt | 27 ++++ .../item/event/EventTimelineItemMapper.kt | 15 +- 10 files changed, 332 insertions(+), 11 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldPosition.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index f6b58e2799..e386efc781 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -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.timeline.item.TimelineItemDebugInfo 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.aProfileTimelineDetailsReady import kotlinx.collections.immutable.ImmutableList @@ -137,6 +138,7 @@ internal fun aTimelineItemEvent( debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(), readReceiptState: TimelineItemReadReceipts = aTimelineItemReadReceipts(), + messageShield: MessageShield? = null, ): TimelineItem.Event { return TimelineItem.Event( id = UUID.randomUUID().toString(), @@ -159,7 +161,8 @@ internal fun aTimelineItemEvent( inReplyTo = inReplyTo, debugInfo = debugInfo, isThreaded = isThreaded, - origin = null + origin = null, + messageShield = messageShield, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldPosition.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldPosition.kt new file mode 100644 index 0000000000..41c424c56e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldPosition.kt @@ -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() +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt new file mode 100644 index 0000000000..decb75fb2e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt @@ -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 + ) + ) + } + } +} 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 a2ac0ac7b1..bc567c1250 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 @@ -17,6 +17,7 @@ package io.element.android.features.messages.impl.timeline.components import android.annotation.SuppressLint +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable 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.semantics import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.IntOffset 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.TimelineItemVideoContent 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.aTimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo @@ -277,6 +281,7 @@ private fun TimelineItemEventRowContent( val ( sender, message, + shield, reactions, ) = 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 if (event.reactionsState.reactions.isNotEmpty()) { TimelineItemReactionsView( @@ -339,7 +367,11 @@ private fun TimelineItemEventRowContent( onMoreReactionsClick = { onMoreReactionsClick(event) }, modifier = Modifier .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) } .zIndex(1f) @@ -472,6 +504,7 @@ private fun MessageEventBubbleContent( @Composable fun CommonLayout( timestampPosition: TimestampPosition, + messageShieldPosition: MessageShieldPosition, showThreadDecoration: Boolean, inReplyToDetails: InReplyToDetails?, modifier: Modifier = Modifier, @@ -510,13 +543,30 @@ private fun MessageEventBubbleContent( canShrinkContent = canShrinkContent, modifier = timestampLayoutModifier, ) { onContentLayoutChange -> - TimelineItemEventContentView( - content = event.content, - onLinkClick = onLinkClick, - eventSink = eventSink, - onContentLayoutChange = onContentLayoutChange, - modifier = contentModifier - ) + + if (messageShieldPosition is MessageShieldPosition.InBubble) { + Column { + TimelineItemEventContentView( + content = event.content, + 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 -> @@ -551,9 +601,11 @@ private fun MessageEventBubbleContent( is TimelineItemPollContent -> TimestampPosition.Below else -> TimestampPosition.Default } + val messageShieldPosition = event.shieldPosition() CommonLayout( showThreadDecoration = event.isThreaded, - timestampPosition = timestampPosition, + messageShieldPosition = messageShieldPosition, + timestampPosition = if (messageShieldPosition is MessageShieldPosition.InBubble) TimestampPosition.Below else timestampPosition, inReplyToDetails = event.inReplyTo, canShrinkContent = event.content is TimelineItemVoiceContent, 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) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index c5d303b3f1..ae2dd3e39c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt @@ -85,6 +85,7 @@ class TimelineItemEventFactory @Inject constructor( isThreaded = currentTimelineItem.event.isThreaded(), debugInfo = currentTimelineItem.event.debugInfo, origin = currentTimelineItem.event.origin, + messageShield = currentTimelineItem.event.messageShield, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt index f77db70506..73bfac5a9d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt @@ -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.timeline.item.TimelineItemDebugInfo 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.TimelineItemEventOrigin import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName @@ -82,6 +83,7 @@ sealed interface TimelineItem { val isThreaded: Boolean, val debugInfo: TimelineItemDebugInfo, val origin: TimelineItemEventOrigin?, + val messageShield: MessageShield?, ) : TimelineItem { val showSenderInformation = groupPosition.isNew() && !isMine diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt index 29fa048e1f..e103c44d1c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt @@ -21,6 +21,8 @@ import android.text.style.StyleSpan import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.core.text.buildSpannedString 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 class TimelineItemEventContentProvider : PreviewParameterProvider { @@ -102,3 +104,13 @@ fun aTimelineItemStateEventContent( ) = TimelineItemStateEventContent( 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 +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt index fa15f8f096..170fedfa28 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt @@ -38,6 +38,7 @@ data class EventTimelineItem( val content: EventContent, val debugInfo: TimelineItemDebugInfo, val origin: TimelineItemEventOrigin?, + val messageShield: MessageShield?, ) { fun inReplyTo(): InReplyTo? { return (content as? MessageContent)?.inReplyTo diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt new file mode 100644 index 0000000000..58fc96d8b1 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt @@ -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 +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt index dd0bdabd7f..f80b877baa 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt @@ -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.EventTimelineItem 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.ReactionSender 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 kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList 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.EventTimelineItem as RustEventTimelineItem import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo @@ -55,7 +58,8 @@ class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMap timestamp = it.timestamp().toLong(), content = contentMapper.map(it.content()), 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 } } + +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 + } +} From 1a671f708e3f739e3822a5d5f783061157ebb282 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 25 Jul 2024 09:48:58 +0200 Subject: [PATCH 02/13] Message Shields - i18n --- .../components/TimelineItemEventRow.kt | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) 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 bc567c1250..486732d7c7 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 @@ -37,6 +37,7 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -696,14 +697,29 @@ internal fun TimelineItemEventRowShieldsPreview() = ElementPreview { } } +@Composable +@ReadOnlyComposable private fun TimelineItem.Event.shieldPosition(): MessageShieldPosition { - if (this.messageShield == null) return MessageShieldPosition.None + val shield = this.messageShield ?: return MessageShieldPosition.None + + // sdk returns raw human readable strings, add i18n support + val localizedMessage = when (shield.message) { + "The authenticity of this encrypted message can't be guaranteed on this device." -> stringResource( + CommonStrings.event_shield_reason_authenticity_not_guaranteed + ) + "Encrypted by a device not verified by its owner." -> stringResource(CommonStrings.event_shield_reason_unsigned_device) + "Encrypted by an unknown or deleted device." -> stringResource(CommonStrings.event_shield_reason_unknown_device) + "Encrypted by an unverified user." -> stringResource(CommonStrings.event_shield_reason_unverified_identity) + else -> shield.message + } + val localShield = shield.copy(message = localizedMessage) + return when (this.content) { is TimelineItemImageContent, is TimelineItemVideoContent, is TimelineItemStickerContent, is TimelineItemLocationContent, - is TimelineItemPollContent -> MessageShieldPosition.OutOfBubble(this.messageShield) - else -> MessageShieldPosition.InBubble(this.messageShield) + is TimelineItemPollContent -> MessageShieldPosition.OutOfBubble(localShield) + else -> MessageShieldPosition.InBubble(localShield) } } From 82abc9f6f87b11bc6ff98df7752775166ece43c9 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 25 Jul 2024 10:00:36 +0200 Subject: [PATCH 03/13] MessageShields | Fix test compilation --- .../android/libraries/matrix/test/timeline/TimelineFixture.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt index d53bc7b18f..8be4da3876 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt @@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventTimeline import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent +import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import io.element.android.libraries.matrix.api.timeline.item.event.MessageType import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent @@ -57,6 +58,7 @@ fun anEventTimelineItem( timestamp: Long = 0L, content: EventContent = aProfileChangeMessageContent(), debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), + messageShield: MessageShield? = null, ) = EventTimelineItem( eventId = eventId, transactionId = transactionId, @@ -73,6 +75,7 @@ fun anEventTimelineItem( content = content, debugInfo = debugInfo, origin = null, + messageShield = messageShield, ) fun aProfileTimelineDetails( From 61e091ca42d24b34c193ae8e9942ee82f9e240a3 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 25 Jul 2024 10:22:15 +0200 Subject: [PATCH 04/13] review: Konsist fix --- .../messages/impl/timeline/components/MessageShieldView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt index decb75fb2e..2efa888bb7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt @@ -87,7 +87,7 @@ private fun MessageShield.toIcon(): ImageVector { @PreviewsDayNight @Composable -internal fun MessageShieldViewPreviews() { +internal fun MessageShieldViewPreview() { ElementPreview { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { MessageShieldView( From 34268a30ea5c682dcce2929fb5803b7805874409 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 14 Aug 2024 17:02:21 +0200 Subject: [PATCH 05/13] Iterate on shield mapping and rendering Also handle click on the timeline and information displayed on long click. --- .../actionlist/ActionListStateProvider.kt | 11 ++ .../impl/actionlist/ActionListView.kt | 10 +- .../messages/impl/timeline/TimelineEvents.kt | 4 + .../impl/timeline/TimelinePresenter.kt | 5 + .../messages/impl/timeline/TimelineState.kt | 3 + .../impl/timeline/TimelineStateProvider.kt | 2 + .../messages/impl/timeline/TimelineView.kt | 19 ++ .../components/ATimelineItemEventRow.kt | 1 + .../components/MessageShieldPosition.kt | 25 --- .../timeline/components/MessageShieldView.kt | 103 ++++++----- .../components/TimelineEventTimestampView.kt | 29 +++- ...melineItemEventForTimestampViewProvider.kt | 7 + .../components/TimelineItemEventRow.kt | 164 +++--------------- .../TimelineItemEventRowShieldPreview.kt | 78 +++++++++ .../TimelineItemGroupedEventsRow.kt | 7 + .../timeline/components/TimelineItemRow.kt | 4 + .../event/TimelineItemEventContentProvider.kt | 12 -- .../impl/fixtures/MessageEventFixtures.kt | 5 +- .../groups/TimelineItemGrouperTest.kt | 3 +- .../RedactedVoiceMessageManagerTest.kt | 3 +- .../components/dialogs/AlertDialog.kt | 92 ++++++++++ .../api/timeline/item/event/MessageShield.kt | 34 +++- .../item/event/EventTimelineItemMapper.kt | 22 ++- .../tests/konsist/KonsistPreviewTest.kt | 1 + 24 files changed, 399 insertions(+), 245 deletions(-) delete mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldPosition.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowShieldPreview.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/AlertDialog.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt index 48c44e38c9..41c5074947 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt @@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent +import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -121,6 +122,16 @@ open class ActionListStateProvider : PreviewParameterProvider { actions = aTimelineItemPollActionList(), ), ), + anActionListState().copy( + target = ActionListState.Target.Success( + event = aTimelineItemEvent().copy( + reactionsState = reactionsState, + messageShield = MessageShield.UnknownDevice(isCritical = true) + ), + displayEmojiReactions = true, + actions = aTimelineItemActionList(), + ) + ), ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index eb2cda8ca7..d928219fe5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -55,6 +55,7 @@ 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.features.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.timeline.components.MessageShieldView import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent @@ -181,7 +182,14 @@ private fun SheetContent( .fillMaxWidth() .padding(horizontal = 16.dp) ) - Spacer(modifier = Modifier.height(14.dp)) + if (target.event.messageShield != null) { + MessageShieldView( + shield = target.event.messageShield, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp) + ) + } else { + Spacer(modifier = Modifier.height(14.dp)) + } HorizontalDivider() } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt index 25ed908cd0..589ca8c6c8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import kotlin.time.Duration sealed interface TimelineEvents { @@ -27,6 +28,9 @@ sealed interface TimelineEvents { data object OnFocusEventRender : TimelineEvents data object JumpToLive : TimelineEvents + data class ShowShieldDialog(val messageShield: MessageShield) : TimelineEvents + data object HideShieldDialog : TimelineEvents + /** * Events coming from a timeline item. */ diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index 3f7db9607a..c7e3dc6e98 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -45,6 +45,7 @@ import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.isDm import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.timeline.ReceiptType +import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.matrix.ui.room.canSendMessageAsState import io.element.android.libraries.preferences.api.store.SessionPreferencesStore @@ -97,6 +98,7 @@ class TimelinePresenter @AssistedInject constructor( val prevMostRecentItemId = rememberSaveable { mutableStateOf(null) } val newEventState = remember { mutableStateOf(NewEventState.None) } + val messageShield: MutableState = remember { mutableStateOf(null) } val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true) val renderReadReceipts by sessionPreferencesStore.isRenderReadReceiptsEnabled().collectAsState(initial = true) @@ -151,6 +153,8 @@ class TimelinePresenter @AssistedInject constructor( is TimelineEvents.JumpToLive -> { timelineController.focusOnLive() } + TimelineEvents.HideShieldDialog -> messageShield.value = null + is TimelineEvents.ShowShieldDialog -> messageShield.value = event.messageShield } } @@ -226,6 +230,7 @@ class TimelinePresenter @AssistedInject constructor( newEventState = newEventState.value, isLive = isLive, focusRequestState = focusRequestState.value, + messageShield = messageShield.value, eventSink = { handleEvents(it) } ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt index 9339fcb620..74f8fda0b4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import kotlinx.collections.immutable.ImmutableList import kotlin.time.Duration @@ -31,6 +32,8 @@ data class TimelineState( val newEventState: NewEventState, val isLive: Boolean, val focusRequestState: FocusRequestState, + // If not null, info will be rendered in a dialog + val messageShield: MessageShield?, val eventSink: (TimelineEvents) -> Unit, ) { val hasAnyEvent = timelineItems.any { it is TimelineItem.Event } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index e40a1dd78a..677989a81a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -51,6 +51,7 @@ fun aTimelineState( timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(), focusedEventIndex: Int = -1, isLive: Boolean = true, + messageShield: MessageShield? = null, eventSink: (TimelineEvents) -> Unit = {}, ): TimelineState { val focusedEventId = timelineItems.filterIsInstance().getOrNull(focusedEventIndex)?.eventId @@ -66,6 +67,7 @@ fun aTimelineState( newEventState = NewEventState.None, isLive = isLive, focusRequestState = focusRequestState, + messageShield = messageShield, eventSink = eventSink, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 2b62071c19..62aa351cc4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -58,6 +58,7 @@ 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.features.messages.impl.timeline.components.TimelineItemRow +import io.element.android.features.messages.impl.timeline.components.toText import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.focus.FocusRequestStateView @@ -68,12 +69,14 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.messages.impl.typing.TypingNotificationView import io.element.android.features.messages.impl.typing.aTypingNotificationState +import io.element.android.libraries.designsystem.components.dialogs.AlertDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.FloatingActionButton import io.element.android.libraries.designsystem.theme.components.Icon 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.timeline.item.event.MessageShield import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.launch import kotlin.math.abs @@ -124,6 +127,10 @@ fun TimelineView( state.eventSink(TimelineEvents.FocusOnEvent(eventId)) } + fun onShieldClick(shield: MessageShield) { + state.eventSink(TimelineEvents.ShowShieldDialog(shield)) + } + // Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms AnimatedVisibility(visible = true, enter = fadeIn()) { Box(modifier) { @@ -154,6 +161,7 @@ fun TimelineView( focusedEventId = state.focusedEventId, onClick = onMessageClick, onLongClick = onMessageLongClick, + onShieldClick = ::onShieldClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, inReplyToClick = ::inReplyToClick, @@ -186,6 +194,17 @@ fun TimelineView( ) } } + + MessageShieldDialog(state) +} + +@Composable +private fun MessageShieldDialog(state: TimelineState) { + val messageShield = state.messageShield ?: return + AlertDialog( + content = messageShield.toText(), + onDismiss = { state.eventSink.invoke(TimelineEvents.HideShieldDialog) }, + ) } @Composable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt index 8e482c020f..959b02bc4a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt @@ -37,6 +37,7 @@ internal fun ATimelineItemEventRow( isHighlighted = isHighlighted, onClick = {}, onLongClick = {}, + onShieldClick = {}, onUserDataClick = {}, onLinkClick = {}, inReplyToClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldPosition.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldPosition.kt deleted file mode 100644 index 41c424c56e..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldPosition.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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() -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt index 2efa888bb7..649c03b985 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt @@ -16,19 +16,18 @@ 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.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons @@ -36,52 +35,71 @@ 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 +import io.element.android.libraries.matrix.api.timeline.item.event.isCritical +import io.element.android.libraries.ui.strings.CommonStrings @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) + verticalAlignment = Alignment.CenterVertically, + modifier = modifier, ) { Icon( imageVector = shield.toIcon(), contentDescription = null, modifier = Modifier.size(15.dp), - tint = iconColor, + tint = shield.toIconColor(), ) 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 + text = shield.toText(), + style = ElementTheme.typography.fontBodySmMedium, + color = shield.toTextColor() ) } } @Composable -private fun MessageShield.toIcon(): ImageVector { - return when (this.color) { - ShieldColor.RED -> CompoundIcons.Error() - ShieldColor.GREY -> CompoundIcons.InfoSolid() +internal fun MessageShield.toIconColor(): Color { + return when (isCritical()) { + true -> ElementTheme.colors.iconCriticalPrimary + false -> ElementTheme.colors.iconSecondary + } +} + +@Composable +private fun MessageShield.toTextColor(): Color { + return when (isCritical()) { + true -> ElementTheme.colors.textCriticalPrimary + false -> ElementTheme.colors.textSecondary + } +} + +@Composable +internal fun MessageShield.toText(): String { + return stringResource( + id = when (this) { + is MessageShield.AuthenticityNotGuaranteed -> CommonStrings.event_shield_reason_authenticity_not_guaranteed + is MessageShield.UnknownDevice -> CommonStrings.event_shield_reason_unknown_device + is MessageShield.UnsignedDevice -> CommonStrings.event_shield_reason_unsigned_device + is MessageShield.UnverifiedIdentity -> CommonStrings.event_shield_reason_unverified_identity + is MessageShield.SentInClear -> CommonStrings.event_shield_reason_sent_in_clear + } + ) +} + +@Composable +internal fun MessageShield.toIcon(): ImageVector { + return when (this) { + is MessageShield.AuthenticityNotGuaranteed, + is MessageShield.UnverifiedIdentity -> CompoundIcons.Admin() + is MessageShield.UnknownDevice, + is MessageShield.UnsignedDevice -> CompoundIcons.HelpSolid() + is MessageShield.SentInClear -> CompoundIcons.KeyOff() } } @@ -89,31 +107,24 @@ private fun MessageShield.toIcon(): ImageVector { @Composable internal fun MessageShieldViewPreview() { ElementPreview { - Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + 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 - ) + shield = MessageShield.UnknownDevice(true) ) MessageShieldView( - isMine = true, - shield = MessageShield( - message = "The authenticity of this encrypted message can't be guaranteed on this device.", - color = ShieldColor.GREY - ) + shield = MessageShield.UnverifiedIdentity(true) ) MessageShieldView( - shield = MessageShield( - message = "Encrypted by a device not verified by its owner.", - color = ShieldColor.RED - ) + shield = MessageShield.AuthenticityNotGuaranteed(false) ) MessageShieldView( - shield = MessageShield( - message = "Encrypted by an unknown or deleted device.", - color = ShieldColor.RED - ) + shield = MessageShield.UnsignedDevice(false) + ) + MessageShieldView( + shield = MessageShield.SentInClear(false) ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt index 25181d7eb3..eb57e51006 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt @@ -16,6 +16,7 @@ package io.element.android.features.messages.impl.timeline.components +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -33,26 +34,31 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.isEdited +import io.element.android.libraries.core.bool.orFalse 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.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.isCritical import io.element.android.libraries.ui.strings.CommonStrings @Composable fun TimelineEventTimestampView( event: TimelineItem.Event, + onShieldClick: (MessageShield) -> Unit, modifier: Modifier = Modifier, ) { val formattedTime = event.sentTime val hasUnrecoverableError = event.localSendState is LocalEventSendState.SendingFailed.Unrecoverable + val hasEncryptionCritical = event.messageShield?.isCritical().orFalse() val isMessageEdited = event.content.isEdited() - val tint = if (hasUnrecoverableError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary + val tint = if (hasUnrecoverableError || hasEncryptionCritical) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary Row( modifier = Modifier - .padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing)) - .then(modifier), + .padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing)) + .then(modifier), verticalAlignment = Alignment.CenterVertically, ) { if (isMessageEdited) { @@ -77,13 +83,28 @@ fun TimelineEventTimestampView( modifier = Modifier.size(15.dp, 18.dp), ) } + event.messageShield?.let { shield -> + Spacer(modifier = Modifier.width(2.dp)) + Icon( + imageVector = shield.toIcon(), + contentDescription = null, + modifier = Modifier + .size(15.dp) + .clickable { onShieldClick(shield) }, + tint = shield.toIconColor(), + ) + Spacer(modifier = Modifier.width(4.dp)) + } } } @PreviewsDayNight @Composable internal fun TimelineEventTimestampViewPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) = ElementPreview { - TimelineEventTimestampView(event = event) + TimelineEventTimestampView( + event = event, + onShieldClick = {}, + ) } object TimelineEventTimestampViewDefaults { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt index 84a93f1c09..705ac36df5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt @@ -21,6 +21,7 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState +import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield class TimelineItemEventForTimestampViewProvider : PreviewParameterProvider { override val values: Sequence @@ -37,5 +38,11 @@ class TimelineItemEventForTimestampViewProvider : PreviewParameterProvider Unit, onLongClick: () -> Unit, + onShieldClick: (MessageShield) -> Unit, onLinkClick: (String) -> Unit, onUserDataClick: (UserId) -> Unit, inReplyToClick: (EventId) -> Unit, @@ -185,6 +182,7 @@ fun TimelineItemEventRow( interactionSource = interactionSource, onClick = onClick, onLongClick = onLongClick, + onShieldClick = onShieldClick, inReplyToClick = ::inReplyToClick, onUserDataClick = ::onUserDataClick, onReactionClick = { emoji -> onReactionClick(emoji, event) }, @@ -203,6 +201,7 @@ fun TimelineItemEventRow( interactionSource = interactionSource, onClick = onClick, onLongClick = onLongClick, + onShieldClick = onShieldClick, inReplyToClick = ::inReplyToClick, onUserDataClick = ::onUserDataClick, onReactionClick = { emoji -> onReactionClick(emoji, event) }, @@ -258,6 +257,7 @@ private fun TimelineItemEventRowContent( interactionSource: MutableInteractionSource, onClick: () -> Unit, onLongClick: () -> Unit, + onShieldClick: (MessageShield) -> Unit, inReplyToClick: () -> Unit, onUserDataClick: () -> Unit, onReactionClick: (emoji: String) -> Unit, @@ -281,7 +281,6 @@ private fun TimelineItemEventRowContent( val ( sender, message, - shield, reactions, ) = createRefs() @@ -326,6 +325,7 @@ private fun TimelineItemEventRowContent( ) { MessageEventBubbleContent( event = event, + onShieldClick = onShieldClick, onMessageLongClick = onLongClick, inReplyToClick = inReplyToClick, onLinkClick = onLinkClick, @@ -333,29 +333,6 @@ 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 if (event.reactionsState.reactions.isNotEmpty()) { TimelineItemReactionsView( @@ -367,11 +344,7 @@ private fun TimelineItemEventRowContent( onMoreReactionsClick = { onMoreReactionsClick(event) }, modifier = Modifier .constrainAs(reactions) { - if (shieldPosition is MessageShieldPosition.OutOfBubble) { - top.linkTo(shield.bottom, margin = (-4).dp) - } else { - top.linkTo(message.bottom, margin = (-4).dp) - } + top.linkTo(message.bottom, margin = (-4).dp) linkStartOrEnd(event) } .zIndex(1f) @@ -413,6 +386,7 @@ private fun MessageSenderInformation( @Composable private fun MessageEventBubbleContent( event: TimelineItem.Event, + onShieldClick: (MessageShield) -> Unit, onMessageLongClick: () -> Unit, inReplyToClick: () -> Unit, onLinkClick: (String) -> Unit, @@ -453,6 +427,7 @@ private fun MessageEventBubbleContent( @Composable fun WithTimestampLayout( timestampPosition: TimestampPosition, + onShieldClick: (MessageShield) -> Unit, modifier: Modifier = Modifier, canShrinkContent: Boolean = false, content: @Composable (onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit) -> Unit, @@ -463,6 +438,7 @@ private fun MessageEventBubbleContent( content {} TimelineEventTimestampView( event = event, + onShieldClick = onShieldClick, modifier = Modifier // Outer padding .padding(horizontal = 4.dp, vertical = 4.dp) @@ -483,6 +459,7 @@ private fun MessageEventBubbleContent( overlay = { TimelineEventTimestampView( event = event, + onShieldClick = onShieldClick, modifier = Modifier .padding(horizontal = 8.dp, vertical = 4.dp) ) @@ -493,6 +470,7 @@ private fun MessageEventBubbleContent( content {} TimelineEventTimestampView( event = event, + onShieldClick = onShieldClick, modifier = Modifier .align(Alignment.End) .padding(horizontal = 8.dp, vertical = 4.dp) @@ -505,7 +483,6 @@ private fun MessageEventBubbleContent( @Composable fun CommonLayout( timestampPosition: TimestampPosition, - messageShieldPosition: MessageShieldPosition, showThreadDecoration: Boolean, inReplyToDetails: InReplyToDetails?, modifier: Modifier = Modifier, @@ -541,35 +518,20 @@ private fun MessageEventBubbleContent( val contentWithTimestamp = @Composable { WithTimestampLayout( timestampPosition = timestampPosition, + onShieldClick = onShieldClick, canShrinkContent = canShrinkContent, modifier = timestampLayoutModifier, ) { onContentLayoutChange -> - - if (messageShieldPosition is MessageShieldPosition.InBubble) { - Column { - TimelineItemEventContentView( - content = event.content, - 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 - ) - } + TimelineItemEventContentView( + content = event.content, + onLinkClick = onLinkClick, + eventSink = eventSink, + onContentLayoutChange = onContentLayoutChange, + modifier = contentModifier + ) } } + val inReplyTo = @Composable { inReplyTo: InReplyToDetails -> val topPadding = if (showThreadDecoration) 0.dp else 8.dp val inReplyToModifier = Modifier @@ -602,11 +564,9 @@ private fun MessageEventBubbleContent( is TimelineItemPollContent -> TimestampPosition.Below else -> TimestampPosition.Default } - val messageShieldPosition = event.shieldPosition() CommonLayout( showThreadDecoration = event.isThreaded, - messageShieldPosition = messageShieldPosition, - timestampPosition = if (messageShieldPosition is MessageShieldPosition.InBubble) TimestampPosition.Below else timestampPosition, + timestampPosition = timestampPosition, inReplyToDetails = event.inReplyTo, canShrinkContent = event.content is TimelineItemVoiceContent, modifier = bubbleModifier.semantics(mergeDescendants = true) { @@ -643,83 +603,3 @@ 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() - ), - ) - } -} - -@Composable -@ReadOnlyComposable -private fun TimelineItem.Event.shieldPosition(): MessageShieldPosition { - val shield = this.messageShield ?: return MessageShieldPosition.None - - // sdk returns raw human readable strings, add i18n support - val localizedMessage = when (shield.message) { - "The authenticity of this encrypted message can't be guaranteed on this device." -> stringResource( - CommonStrings.event_shield_reason_authenticity_not_guaranteed - ) - "Encrypted by a device not verified by its owner." -> stringResource(CommonStrings.event_shield_reason_unsigned_device) - "Encrypted by an unknown or deleted device." -> stringResource(CommonStrings.event_shield_reason_unknown_device) - "Encrypted by an unverified user." -> stringResource(CommonStrings.event_shield_reason_unverified_identity) - else -> shield.message - } - val localShield = shield.copy(message = localizedMessage) - - return when (this.content) { - is TimelineItemImageContent, - is TimelineItemVideoContent, - is TimelineItemStickerContent, - is TimelineItemLocationContent, - is TimelineItemPollContent -> MessageShieldPosition.OutOfBubble(localShield) - else -> MessageShieldPosition.InBubble(localShield) - } -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowShieldPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowShieldPreview.kt new file mode 100644 index 0000000000..00be5d692c --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowShieldPreview.kt @@ -0,0 +1,78 @@ +/* + * 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.foundation.layout.Column +import androidx.compose.runtime.Composable +import io.element.android.features.messages.impl.timeline.aTimelineItemEvent +import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition +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.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield + +@PreviewsDayNight +@Composable +internal fun TimelineItemEventRowShieldPreview() = 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 = aCriticalShield() + ), + ) + 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 = aWarningShield() + ), + ) + ATimelineItemEventRow( + event = aTimelineItemEvent( + isMine = true, + content = aTimelineItemImageContent().copy( + aspectRatio = 2.5f + ), + groupPosition = TimelineItemGroupPosition.Last, + messageShield = aCriticalShield() + ), + ) + ATimelineItemEventRow( + event = aTimelineItemEvent( + content = aTimelineItemImageContent().copy( + aspectRatio = 2.5f + ), + groupPosition = TimelineItemGroupPosition.Last, + messageShield = aWarningShield() + ), + ) + } +} + +private fun aWarningShield() = MessageShield.AuthenticityNotGuaranteed(isCritical = false) + +private fun aCriticalShield() = MessageShield.UnverifiedIdentity(isCritical = true) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt index 4dd4d4e356..bb1cb2b2df 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt @@ -36,6 +36,7 @@ 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 import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield @Composable fun TimelineItemGroupedEventsRow( @@ -46,6 +47,7 @@ fun TimelineItemGroupedEventsRow( focusedEventId: EventId?, onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, + onShieldClick: (MessageShield) -> Unit, inReplyToClick: (EventId) -> Unit, onUserDataClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, @@ -72,6 +74,7 @@ fun TimelineItemGroupedEventsRow( isLastOutgoingMessage = isLastOutgoingMessage, onClick = onClick, onLongClick = onLongClick, + onShieldClick = onShieldClick, inReplyToClick = inReplyToClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, @@ -95,6 +98,7 @@ private fun TimelineItemGroupedEventsRowContent( isLastOutgoingMessage: Boolean, onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, + onShieldClick: (MessageShield) -> Unit, inReplyToClick: (EventId) -> Unit, onUserDataClick: (UserId) -> Unit, onLinkClick: (String) -> Unit, @@ -127,6 +131,7 @@ private fun TimelineItemGroupedEventsRowContent( focusedEventId = focusedEventId, onClick = onClick, onLongClick = onLongClick, + onShieldClick = onShieldClick, inReplyToClick = inReplyToClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, @@ -168,6 +173,7 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi isLastOutgoingMessage = false, onClick = {}, onLongClick = {}, + onShieldClick = {}, inReplyToClick = {}, onUserDataClick = {}, onLinkClick = {}, @@ -192,6 +198,7 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi isLastOutgoingMessage = false, onClick = {}, onLongClick = {}, + onShieldClick = {}, inReplyToClick = {}, onUserDataClick = {}, onLinkClick = {}, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 2a3a0305c2..9cbcf20bef 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -37,6 +37,7 @@ import io.element.android.libraries.designsystem.text.toPx import io.element.android.libraries.designsystem.theme.highlightedMessageBackgroundColor 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.timeline.item.event.MessageShield @Composable internal fun TimelineItemRow( @@ -49,6 +50,7 @@ internal fun TimelineItemRow( onLinkClick: (String) -> Unit, onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, + onShieldClick: (MessageShield) -> Unit, inReplyToClick: (EventId) -> Unit, onReactionClick: (key: String, TimelineItem.Event) -> Unit, onReactionLongClick: (key: String, TimelineItem.Event) -> Unit, @@ -110,6 +112,7 @@ internal fun TimelineItemRow( isHighlighted = timelineItem.isEvent(focusedEventId), onClick = { onClick(timelineItem) }, onLongClick = { onLongClick(timelineItem) }, + onShieldClick = onShieldClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, inReplyToClick = inReplyToClick, @@ -132,6 +135,7 @@ internal fun TimelineItemRow( focusedEventId = focusedEventId, onClick = onClick, onLongClick = onLongClick, + onShieldClick = onShieldClick, inReplyToClick = inReplyToClick, onUserDataClick = onUserDataClick, onLinkClick = onLinkClick, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt index e103c44d1c..29fa048e1f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt @@ -21,8 +21,6 @@ import android.text.style.StyleSpan import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.core.text.buildSpannedString 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 class TimelineItemEventContentProvider : PreviewParameterProvider { @@ -104,13 +102,3 @@ fun aTimelineItemStateEventContent( ) = TimelineItemStateEventContent( 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 -) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt index ca805d44c7..c712421f23 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt @@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.TransactionId 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.MessageShield import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_USER_ID @@ -48,6 +49,7 @@ internal fun aMessageEvent( isThreaded: Boolean = false, debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), sendState: LocalEventSendState = LocalEventSendState.Sent(AN_EVENT_ID), + messageShield: MessageShield? = null, ) = TimelineItem.Event( id = eventId?.value.orEmpty(), eventId = eventId, @@ -66,5 +68,6 @@ internal fun aMessageEvent( inReplyTo = inReplyTo, debugInfo = debugInfo, isThreaded = isThreaded, - origin = null + origin = null, + messageShield = messageShield, ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt index e044ac7e2b..fc98b34feb 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt @@ -50,7 +50,8 @@ class TimelineItemGrouperTest { inReplyTo = null, isThreaded = false, debugInfo = aTimelineItemDebugInfo(), - origin = null + origin = null, + messageShield = null, ) private val aNonGroupableItem = aMessageEvent() private val aNonGroupableItemNoEvent = TimelineItem.Virtual("virtual", aTimelineItemDaySeparatorModel("Today")) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt index 2a54edad58..93882d5f3e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt @@ -101,7 +101,8 @@ fun aRedactedMatrixTimeline(eventId: EventId) = listOf( originalJson = null, latestEditedJson = null ), - origin = null + origin = null, + messageShield = null, ), ) ) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/AlertDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/AlertDialog.kt new file mode 100644 index 0000000000..f22a573e77 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/AlertDialog.kt @@ -0,0 +1,92 @@ +/* + * 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.libraries.designsystem.components.dialogs + +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.ElementThemedPreview +import io.element.android.libraries.designsystem.preview.PreviewGroup +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.DialogPreview +import io.element.android.libraries.designsystem.theme.components.SimpleAlertDialogContent +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AlertDialog( + content: String, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + title: String? = null, + submitText: String = AlertDialogDefaults.submitText, +) { + BasicAlertDialog(modifier = modifier, onDismissRequest = onDismiss) { + AlertDialogContent( + title = title, + content = content, + submitText = submitText, + onSubmitClick = onDismiss, + ) + } +} + +@Composable +private fun AlertDialogContent( + content: String, + onSubmitClick: () -> Unit, + title: String? = AlertDialogDefaults.title, + submitText: String = AlertDialogDefaults.submitText, +) { + SimpleAlertDialogContent( + title = title, + content = content, + submitText = submitText, + onSubmitClick = onSubmitClick, + ) +} + +object AlertDialogDefaults { + val title: String? @Composable get() = null + val submitText: String @Composable get() = stringResource(id = CommonStrings.action_ok) +} + +@Preview(group = PreviewGroup.Dialogs) +@Composable +internal fun AlertDialogContentPreview() { + ElementThemedPreview(showBackground = false) { + DialogPreview { + AlertDialogContent( + content = "Content", + onSubmitClick = {}, + ) + } + } +} + +@PreviewsDayNight +@Composable +internal fun AlertDialogPreview() = ElementPreview { + AlertDialog( + content = "Content", + onDismiss = {}, + ) +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt index 58fc96d8b1..7323f9b712 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt @@ -16,12 +16,32 @@ package io.element.android.libraries.matrix.api.timeline.item.event -data class MessageShield( - val message: String, - val color: ShieldColor, -) +import androidx.compose.runtime.Immutable -enum class ShieldColor { - RED, - GREY +@Immutable +sealed interface MessageShield { + /** Not enough information available to check the authenticity.*/ + data class AuthenticityNotGuaranteed(val isCritical: Boolean) : MessageShield + + /** The sending device isn't yet known by the Client.*/ + data class UnknownDevice(val isCritical: Boolean) : MessageShield + + /** The sending device hasn't been verified by the sender.*/ + data class UnsignedDevice(val isCritical: Boolean) : MessageShield + + /** The sender hasn't been verified by the Client's user.*/ + data class UnverifiedIdentity(val isCritical: Boolean) : MessageShield + + /** An unencrypted event in an encrypted room.*/ + data class SentInClear(val isCritical: Boolean) : MessageShield +} + +fun MessageShield.isCritical(): Boolean { + return when (this) { + is MessageShield.AuthenticityNotGuaranteed -> isCritical + is MessageShield.UnknownDevice -> isCritical + is MessageShield.UnsignedDevice -> isCritical + is MessageShield.UnverifiedIdentity -> isCritical + is MessageShield.SentInClear -> isCritical + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt index 1c5f25a14e..523b8e1fb5 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt @@ -27,13 +27,13 @@ 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.ReactionSender 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 kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import org.matrix.rustcomponents.sdk.Reaction import org.matrix.rustcomponents.sdk.ShieldState +import uniffi.matrix_sdk_common.ShieldStateCode import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo @@ -135,10 +135,22 @@ private fun RustEventItemOrigin.map(): TimelineItemEventOrigin { } 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) + this ?: return null + val shieldStateCode = when (this) { + is ShieldState.Grey -> code + is ShieldState.Red -> code + ShieldState.None -> null + } ?: return null + val isCritical = when (this) { ShieldState.None, - null -> null + is ShieldState.Grey -> false + is ShieldState.Red -> true + } + return when (shieldStateCode) { + ShieldStateCode.AUTHENTICITY_NOT_GUARANTEED -> MessageShield.AuthenticityNotGuaranteed(isCritical) + ShieldStateCode.UNKNOWN_DEVICE -> MessageShield.UnknownDevice(isCritical) + ShieldStateCode.UNSIGNED_DEVICE -> MessageShield.UnsignedDevice(isCritical) + ShieldStateCode.UNVERIFIED_IDENTITY -> MessageShield.UnverifiedIdentity(isCritical) + ShieldStateCode.SENT_IN_CLEAR -> MessageShield.SentInClear(isCritical) } } diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index ccf6754fc5..3fb609b0b5 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -120,6 +120,7 @@ class KonsistPreviewTest { "TextComposerVoicePreview", "TimelineImageWithCaptionRowPreview", "TimelineItemEventRowForDirectRoomPreview", + "TimelineItemEventRowShieldPreview", "TimelineItemEventRowTimestampPreview", "TimelineItemEventRowWithManyReactionsPreview", "TimelineItemEventRowWithRRPreview", From 05f02f2aa7e4ab582dc13e2cbb55654cf915bd56 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 14 Aug 2024 15:49:23 +0000 Subject: [PATCH 06/13] Update screenshots --- ...eatures.messages.impl.actionlist_SheetContent_Day_11_en.png | 3 +++ ...tures.messages.impl.actionlist_SheetContent_Night_11_en.png | 3 +++ ...ges.impl.timeline.components_MessageShieldView_Day_0_en.png | 3 +++ ...s.impl.timeline.components_MessageShieldView_Night_0_en.png | 3 +++ ...timeline.components_TimelineEventTimestampView_Day_5_en.png | 3 +++ ...timeline.components_TimelineEventTimestampView_Day_6_en.png | 3 +++ ...meline.components_TimelineEventTimestampView_Night_5_en.png | 3 +++ ...meline.components_TimelineEventTimestampView_Night_6_en.png | 3 +++ ...timeline.components_TimelineItemEventRowShield_Day_0_en.png | 3 +++ ...meline.components_TimelineItemEventRowShield_Night_0_en.png | 3 +++ ...eline.components_TimelineItemEventRowTimestamp_Day_5_en.png | 3 +++ ...eline.components_TimelineItemEventRowTimestamp_Day_6_en.png | 3 +++ ...ine.components_TimelineItemEventRowTimestamp_Night_5_en.png | 3 +++ ...ine.components_TimelineItemEventRowTimestamp_Night_6_en.png | 3 +++ ...system.components.dialogs_AlertDialogContent_Dialogs_en.png | 3 +++ ...es.designsystem.components.dialogs_AlertDialog_Day_0_en.png | 3 +++ ....designsystem.components.dialogs_AlertDialog_Night_0_en.png | 3 +++ 17 files changed, 51 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_SheetContent_Day_11_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_SheetContent_Night_11_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_6_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_6_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_6_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_6_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_AlertDialog_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_AlertDialog_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_SheetContent_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_SheetContent_Day_11_en.png new file mode 100644 index 0000000000..2a383f511b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_SheetContent_Day_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfd9526209bdb36863765802036b08a4a61ae8035afed4f47b262d521d8bd37d +size 45145 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_SheetContent_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_SheetContent_Night_11_en.png new file mode 100644 index 0000000000..2d2c2ad36d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_SheetContent_Night_11_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b82542b62c8a35c59f96c1b348a62f71de0d1302550458f11581ddec65b2172 +size 44342 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Day_0_en.png new file mode 100644 index 0000000000..626e88bcc1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6c05a05fa51e513937162578540f720e138e5263213211b51c0af54c295c6d0 +size 30756 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Night_0_en.png new file mode 100644 index 0000000000..27073d532c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:679652bc8587d10595565c55ac633c84b5650c9cf4a513e03be5de0cb2eaa012 +size 29933 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_5_en.png new file mode 100644 index 0000000000..c477c15bb5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ba7b49b4872d05bb5a277024a8c0786dd7a8b8c3db6462a455cbd73e682602a +size 4910 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_6_en.png new file mode 100644 index 0000000000..3585275158 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f45afae6e96d3cddb50eda9acac94ba7b556e480eccf56e5f7c4c17a7a5bfb7 +size 5009 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_5_en.png new file mode 100644 index 0000000000..efa2ef9520 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1597f66fc8a4054b2dbca912c4cf2d2088692cc727f5b1ecfa0b82f3c3d71bf0 +size 4845 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_6_en.png new file mode 100644 index 0000000000..d5a4f8dde6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e07f82770e48bcd04bf5532dd353d0f84d6128911e692a8e728f1b768d0b947 +size 4988 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Day_0_en.png new file mode 100644 index 0000000000..e2cbde040e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87dfe8aa9c89ac21b273eabc4f92e12681d114daa408fc999bba5b209b47f9d8 +size 148921 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Night_0_en.png new file mode 100644 index 0000000000..771599f98d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89ef14c0376205fa40582bbb882180aacb8729025e65b02c2231fc848d807911 +size 148430 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_5_en.png new file mode 100644 index 0000000000..4050bdcab5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:196ec6c84cc1c975091f8455fdbce2fdf928243c888ee6eeaf32127736686f85 +size 30369 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_6_en.png new file mode 100644 index 0000000000..9a163d48b2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1381e2fd76dbb52a67fb12d588c839b92a5701c2abf3d2fc14d89fead977a35e +size 30727 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_5_en.png new file mode 100644 index 0000000000..0699e39d83 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b266883f18631845e979bdc9f3bc6d375891f5e37d6839873486f367a0a43dc1 +size 30712 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_6_en.png new file mode 100644 index 0000000000..28cb3b58b8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07036aaecb01e2b27e133a2d9f254ef38ff8ab18913a2684db1391aa829bd3ed +size 30945 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en.png new file mode 100644 index 0000000000..6bc3482fa9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_AlertDialogContent_Dialogs_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be622097026463273020202508a7990f358b5eb33103917be2bc272e744d7a76 +size 11310 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_AlertDialog_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_AlertDialog_Day_0_en.png new file mode 100644 index 0000000000..6ef9f48c82 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_AlertDialog_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c01359ad5875334bc4904a3cfb171f8d4cc87e6452591a72435c8b3f116439ac +size 8398 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_AlertDialog_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_AlertDialog_Night_0_en.png new file mode 100644 index 0000000000..0a326effa4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.dialogs_AlertDialog_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e47308ddbd1310aadf5471dbe7c1ba3d22e26555d3f08514ea13b4acc3cf07b8 +size 7048 From 0362498faeda0abb7997686cc51ffdae266aa6f7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Aug 2024 09:18:13 +0200 Subject: [PATCH 07/13] Make extension isCritical a val instead of a fun. --- .../impl/timeline/components/MessageShieldView.kt | 4 ++-- .../components/TimelineEventTimestampView.kt | 2 +- .../api/timeline/item/event/MessageShield.kt | 15 +++++++-------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt index 649c03b985..4b1415e8d4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt @@ -65,7 +65,7 @@ internal fun MessageShieldView( @Composable internal fun MessageShield.toIconColor(): Color { - return when (isCritical()) { + return when (isCritical) { true -> ElementTheme.colors.iconCriticalPrimary false -> ElementTheme.colors.iconSecondary } @@ -73,7 +73,7 @@ internal fun MessageShield.toIconColor(): Color { @Composable private fun MessageShield.toTextColor(): Color { - return when (isCritical()) { + return when (isCritical) { true -> ElementTheme.colors.textCriticalPrimary false -> ElementTheme.colors.textSecondary } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt index eb57e51006..a7500531b2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt @@ -52,7 +52,7 @@ fun TimelineEventTimestampView( ) { val formattedTime = event.sentTime val hasUnrecoverableError = event.localSendState is LocalEventSendState.SendingFailed.Unrecoverable - val hasEncryptionCritical = event.messageShield?.isCritical().orFalse() + val hasEncryptionCritical = event.messageShield?.isCritical.orFalse() val isMessageEdited = event.content.isEdited() val tint = if (hasUnrecoverableError || hasEncryptionCritical) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.secondary Row( diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt index 7323f9b712..9092817569 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt @@ -20,28 +20,27 @@ import androidx.compose.runtime.Immutable @Immutable sealed interface MessageShield { - /** Not enough information available to check the authenticity.*/ + /** Not enough information available to check the authenticity. */ data class AuthenticityNotGuaranteed(val isCritical: Boolean) : MessageShield - /** The sending device isn't yet known by the Client.*/ + /** The sending device isn't yet known by the Client. */ data class UnknownDevice(val isCritical: Boolean) : MessageShield - /** The sending device hasn't been verified by the sender.*/ + /** The sending device hasn't been verified by the sender. */ data class UnsignedDevice(val isCritical: Boolean) : MessageShield - /** The sender hasn't been verified by the Client's user.*/ + /** The sender hasn't been verified by the Client's user. */ data class UnverifiedIdentity(val isCritical: Boolean) : MessageShield - /** An unencrypted event in an encrypted room.*/ + /** An unencrypted event in an encrypted room. */ data class SentInClear(val isCritical: Boolean) : MessageShield } -fun MessageShield.isCritical(): Boolean { - return when (this) { +val MessageShield.isCritical: Boolean + get() = when (this) { is MessageShield.AuthenticityNotGuaranteed -> isCritical is MessageShield.UnknownDevice -> isCritical is MessageShield.UnsignedDevice -> isCritical is MessageShield.UnverifiedIdentity -> isCritical is MessageShield.SentInClear -> isCritical } -} From f280ea6a0de090dd0d5e51595d5cfa18b57f7256 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Aug 2024 09:27:19 +0200 Subject: [PATCH 08/13] Iterate on mapping MessageShield -> icon iOS impl: https://github.com/element-hq/element-x-ios/blob/develop/ElementX/Sources/Services/Timeline/TimelineItemContent/EncryptionAuthenticity.swift#L59-L65 --- .../impl/timeline/components/MessageShieldView.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt index 4b1415e8d4..73d4c9ba88 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt @@ -95,11 +95,11 @@ internal fun MessageShield.toText(): String { @Composable internal fun MessageShield.toIcon(): ImageVector { return when (this) { - is MessageShield.AuthenticityNotGuaranteed, - is MessageShield.UnverifiedIdentity -> CompoundIcons.Admin() + is MessageShield.AuthenticityNotGuaranteed -> CompoundIcons.Info() is MessageShield.UnknownDevice, - is MessageShield.UnsignedDevice -> CompoundIcons.HelpSolid() - is MessageShield.SentInClear -> CompoundIcons.KeyOff() + is MessageShield.UnsignedDevice, + is MessageShield.UnverifiedIdentity -> CompoundIcons.HelpSolid() + is MessageShield.SentInClear -> CompoundIcons.LockOff() } } From 970d11ffd1cba67d4e1ac3cc765e7eac0f4d243a Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 16 Aug 2024 08:07:11 +0000 Subject: [PATCH 09/13] Update screenshots --- ...es.impl.timeline.components_MessageShieldView_Day_0_en.png | 4 ++-- ....impl.timeline.components_MessageShieldView_Night_0_en.png | 4 ++-- ...imeline.components_TimelineEventTimestampView_Day_5_en.png | 4 ++-- ...eline.components_TimelineEventTimestampView_Night_5_en.png | 4 ++-- ...imeline.components_TimelineItemEventRowShield_Day_0_en.png | 4 ++-- ...eline.components_TimelineItemEventRowShield_Night_0_en.png | 4 ++-- ...line.components_TimelineItemEventRowTimestamp_Day_5_en.png | 4 ++-- ...ne.components_TimelineItemEventRowTimestamp_Night_5_en.png | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Day_0_en.png index 626e88bcc1..aa89a718dc 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6c05a05fa51e513937162578540f720e138e5263213211b51c0af54c295c6d0 -size 30756 +oid sha256:18808ed71ef0cbc10be0eda977ab077a0eaa83afbfd2ae50c500b802aa4c7976 +size 30901 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Night_0_en.png index 27073d532c..9b49043ac6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_MessageShieldView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:679652bc8587d10595565c55ac633c84b5650c9cf4a513e03be5de0cb2eaa012 -size 29933 +oid sha256:1d0ad0c7f690d07d67a13fcad3ee8901a77602affe063d2f8222345de3aff934 +size 30039 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_5_en.png index c477c15bb5..b0b3fd1a0b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ba7b49b4872d05bb5a277024a8c0786dd7a8b8c3db6462a455cbd73e682602a -size 4910 +oid sha256:6d37081422eb655aaaa58834912625efd07f61eddcb7117f32b8edb9512969ab +size 5052 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_5_en.png index efa2ef9520..8759060547 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineEventTimestampView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1597f66fc8a4054b2dbca912c4cf2d2088692cc727f5b1ecfa0b82f3c3d71bf0 -size 4845 +oid sha256:6dcce84695624b516035b2a3fe33ad71bf0b278a769ac013ad7c9b4feae958c5 +size 5028 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Day_0_en.png index e2cbde040e..0186b92f5f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87dfe8aa9c89ac21b273eabc4f92e12681d114daa408fc999bba5b209b47f9d8 -size 148921 +oid sha256:c4ba97a7a22e790d2708364c32035d8428677deaa12ff62c0304d252a1043f7a +size 149223 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Night_0_en.png index 771599f98d..6660328620 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89ef14c0376205fa40582bbb882180aacb8729025e65b02c2231fc848d807911 -size 148430 +oid sha256:007de1b45bde33d6a7a8ea389a1b80a07f0696878ecfe34cbcf12c74b866ed09 +size 148817 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_5_en.png index 4050bdcab5..e78b401753 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:196ec6c84cc1c975091f8455fdbce2fdf928243c888ee6eeaf32127736686f85 -size 30369 +oid sha256:99acfb92911fbc7b60126cdadbd5b373f9692258d4f6c59a8167425e7b5603b5 +size 30870 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_5_en.png index 0699e39d83..ce1c266e0a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowTimestamp_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b266883f18631845e979bdc9f3bc6d375891f5e37d6839873486f367a0a43dc1 -size 30712 +oid sha256:7348bb4cc9cde7eda1c7f65ce5f997838add5d8a84974b13bcf6685b15c80f1a +size 31136 From 750ecbee824e18d9e8b9685d002f3629d0272c9e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Aug 2024 10:45:20 +0200 Subject: [PATCH 10/13] Add test on `TimelineEvents.ShowShieldDialog` and `TimelineEvents.HideShieldDialog` --- .../TimelineItemEventRowShieldPreview.kt | 2 +- .../impl/timeline/TimelinePresenterTest.kt | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowShieldPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowShieldPreview.kt index 00be5d692c..828060fd1f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowShieldPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowShieldPreview.kt @@ -75,4 +75,4 @@ internal fun TimelineItemEventRowShieldPreview() = ElementPreview { private fun aWarningShield() = MessageShield.AuthenticityNotGuaranteed(isCritical = false) -private fun aCriticalShield() = MessageShield.UnverifiedIdentity(isCritical = true) +internal fun aCriticalShield() = MessageShield.UnverifiedIdentity(isCritical = true) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index 90c71a964a..a155507817 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -24,6 +24,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.impl.FakeMessagesNavigator import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory +import io.element.android.features.messages.impl.timeline.components.aCriticalShield import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem @@ -591,6 +592,26 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2" } } + @Test + fun `present - show shield hide shield`() = runTest { + val presenter = createTimelinePresenter() + val shield = aCriticalShield() + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitFirstItem() + assertThat(initialState.messageShield).isNull() + initialState.eventSink(TimelineEvents.ShowShieldDialog(shield)) + awaitItem().also { state -> + assertThat(state.messageShield).isEqualTo(shield) + state.eventSink(TimelineEvents.HideShieldDialog) + } + awaitItem().also { state -> + assertThat(state.messageShield).isNull() + } + } + } + @Test fun `present - when room member info is loaded, read receipts info should be updated`() = runTest { val timeline = FakeTimeline( From 0190cb5626078f99f8b800a219a5b0397b56589b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Aug 2024 11:14:51 +0200 Subject: [PATCH 11/13] Add UI test on clicking on message shield. Need to add content description on the shield icon. --- .../components/TimelineEventTimestampView.kt | 2 +- .../impl/timeline/TimelineViewTest.kt | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt index a7500531b2..69c2a807a2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt @@ -87,7 +87,7 @@ fun TimelineEventTimestampView( Spacer(modifier = Modifier.width(2.dp)) Icon( imageVector = shield.toIcon(), - contentDescription = null, + contentDescription = shield.toText(), modifier = Modifier .size(15.dp) .clickable { onShieldClick(shield) }, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index e66bd4c7a1..bb8ea58315 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -22,17 +22,21 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.performClick import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.messages.impl.timeline.components.aCriticalShield import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.messages.impl.typing.aTypingNotificationState import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn import kotlinx.collections.immutable.persistentListOf import org.junit.Rule import org.junit.Test @@ -97,6 +101,47 @@ class TimelineViewTest { rule.onNodeWithContentDescription(contentDescription).performClick() eventsRecorder.assertSingle(TimelineEvents.JumpToLive) } + + @Test + fun `show shield dialog`() { + val eventsRecorder = EventsRecorder() + rule.setTimelineView( + state = aTimelineState( + timelineItems = persistentListOf( + aTimelineItemEvent( + // Do not use a Text because EditorStyledText cannot be used in UI test. + content = aTimelineItemImageContent(), + messageShield = MessageShield.UnverifiedIdentity(true), + ), + ), + eventSink = eventsRecorder, + ), + ) + val contentDescription = rule.activity.getString(CommonStrings.event_shield_reason_unverified_identity) + rule.onNodeWithContentDescription(contentDescription).performClick() + eventsRecorder.assertList( + listOf( + TimelineEvents.OnScrollFinished(0), + TimelineEvents.OnScrollFinished(0), + TimelineEvents.OnScrollFinished(0), + TimelineEvents.ShowShieldDialog(MessageShield.UnverifiedIdentity(true)), + ) + ) + } + + @Test + fun `hide shield dialog`() { + val eventsRecorder = EventsRecorder() + rule.setTimelineView( + state = aTimelineState( + isLive = false, + eventSink = eventsRecorder, + messageShield = aCriticalShield(), + ), + ) + rule.clickOn(CommonStrings.action_ok) + eventsRecorder.assertSingle(TimelineEvents.HideShieldDialog) + } } private fun AndroidComposeTestRule.setTimelineView( From 350c678199f849dbffe242a4808fc35686f49ff5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Aug 2024 11:33:52 +0200 Subject: [PATCH 12/13] Add preview for message shield dialog. --- .../TimelineViewMessageShieldPreview.kt | 62 +++++++++++++++++++ .../tests/konsist/KonsistPreviewTest.kt | 1 + 2 files changed, 63 insertions(+) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt new file mode 100644 index 0000000000..68baf476a3 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewMessageShieldPreview.kt @@ -0,0 +1,62 @@ +/* + * 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 + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import io.element.android.features.messages.impl.timeline.components.aCriticalShield +import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories +import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent +import io.element.android.features.messages.impl.typing.aTypingNotificationState +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import kotlinx.collections.immutable.toImmutableList + +@PreviewsDayNight +@Composable +internal fun TimelineViewMessageShieldPreview() = ElementPreview { + val timelineItems = aTimelineItemList(aTimelineItemTextContent()) + // For consistency, ensure that there is a message in the timeline (the last one) with an error. + val messageShield = aCriticalShield() + val items = listOf( + (timelineItems.first() as TimelineItem.Event).copy(messageShield = messageShield) + ) + timelineItems.drop(1) + CompositionLocalProvider( + LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(), + ) { + TimelineView( + state = aTimelineState( + timelineItems = items.toImmutableList(), + messageShield = messageShield, + ), + typingNotificationState = aTypingNotificationState(), + onUserDataClick = {}, + onLinkClick = {}, + onMessageClick = {}, + onMessageLongClick = {}, + onSwipeToReply = {}, + onReactionClick = { _, _ -> }, + onReactionLongClick = { _, _ -> }, + onMoreReactionsClick = {}, + onReadReceiptClick = {}, + onJoinCallClick = {}, + forceJumpToBottomVisibility = true, + ) + } +} diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index 3fb609b0b5..a64fdaa84c 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -129,6 +129,7 @@ class KonsistPreviewTest { "TimelineItemGroupedEventsRowContentExpandedPreview", "TimelineItemVoiceViewUnifiedPreview", "TimelineVideoWithCaptionRowPreview", + "TimelineViewMessageShieldPreview", "UserAvatarColorsPreview", ) .assertTrue( From 12bdeed60d45c7e2e91743418b9993f7c48ec1d6 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 16 Aug 2024 09:44:36 +0000 Subject: [PATCH 13/13] Update screenshots --- ...ssages.impl.timeline_TimelineViewMessageShield_Day_0_en.png | 3 +++ ...ages.impl.timeline_TimelineViewMessageShield_Night_0_en.png | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en.png new file mode 100644 index 0000000000..d07f02067d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71f9d463136dd01c40bd11d5c4ea571ce65dd4c42f0c724e8c6c4ffec3eb6fa6 +size 37341 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en.png new file mode 100644 index 0000000000..b59834d849 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineViewMessageShield_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1cee48ed80d4a6318e2b1b04873f1f59cab81f7f18046d3d4d31dc27eb26544 +size 35166