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 55a82fa791..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 @@ -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 @@ -50,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 @@ -65,6 +67,7 @@ fun aTimelineState( newEventState = NewEventState.None, isLive = isLive, focusRequestState = focusRequestState, + messageShield = messageShield, eventSink = eventSink, ) } @@ -138,6 +141,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(), @@ -161,7 +165,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/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/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/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/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..73d4c9ba88 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt @@ -0,0 +1,131 @@ +/* + * 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.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.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 +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.MessageShield +import io.element.android.libraries.matrix.api.timeline.item.event.isCritical +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +internal fun MessageShieldView( + shield: MessageShield, + modifier: Modifier = Modifier +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier, + ) { + Icon( + imageVector = shield.toIcon(), + contentDescription = null, + modifier = Modifier.size(15.dp), + tint = shield.toIconColor(), + ) + Spacer(modifier = Modifier.size(4.dp)) + Text( + text = shield.toText(), + style = ElementTheme.typography.fontBodySmMedium, + color = shield.toTextColor() + ) + } +} + +@Composable +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 -> CompoundIcons.Info() + is MessageShield.UnknownDevice, + is MessageShield.UnsignedDevice, + is MessageShield.UnverifiedIdentity -> CompoundIcons.HelpSolid() + is MessageShield.SentInClear -> CompoundIcons.LockOff() + } +} + +@PreviewsDayNight +@Composable +internal fun MessageShieldViewPreview() { + ElementPreview { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + MessageShieldView( + shield = MessageShield.UnknownDevice(true) + ) + MessageShieldView( + shield = MessageShield.UnverifiedIdentity(true) + ) + MessageShieldView( + shield = MessageShield.AuthenticityNotGuaranteed(false) + ) + MessageShieldView( + 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..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 @@ -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 = shield.toText(), + 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, @@ -180,6 +182,7 @@ fun TimelineItemEventRow( interactionSource = interactionSource, onClick = onClick, onLongClick = onLongClick, + onShieldClick = onShieldClick, inReplyToClick = ::inReplyToClick, onUserDataClick = ::onUserDataClick, onReactionClick = { emoji -> onReactionClick(emoji, event) }, @@ -198,6 +201,7 @@ fun TimelineItemEventRow( interactionSource = interactionSource, onClick = onClick, onLongClick = onLongClick, + onShieldClick = onShieldClick, inReplyToClick = ::inReplyToClick, onUserDataClick = ::onUserDataClick, onReactionClick = { emoji -> onReactionClick(emoji, event) }, @@ -253,6 +257,7 @@ private fun TimelineItemEventRowContent( interactionSource: MutableInteractionSource, onClick: () -> Unit, onLongClick: () -> Unit, + onShieldClick: (MessageShield) -> Unit, inReplyToClick: () -> Unit, onUserDataClick: () -> Unit, onReactionClick: (emoji: String) -> Unit, @@ -320,6 +325,7 @@ private fun TimelineItemEventRowContent( ) { MessageEventBubbleContent( event = event, + onShieldClick = onShieldClick, onMessageLongClick = onLongClick, inReplyToClick = inReplyToClick, onLinkClick = onLinkClick, @@ -380,6 +386,7 @@ private fun MessageSenderInformation( @Composable private fun MessageEventBubbleContent( event: TimelineItem.Event, + onShieldClick: (MessageShield) -> Unit, onMessageLongClick: () -> Unit, inReplyToClick: () -> Unit, onLinkClick: (String) -> Unit, @@ -420,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, @@ -430,6 +438,7 @@ private fun MessageEventBubbleContent( content {} TimelineEventTimestampView( event = event, + onShieldClick = onShieldClick, modifier = Modifier // Outer padding .padding(horizontal = 4.dp, vertical = 4.dp) @@ -450,6 +459,7 @@ private fun MessageEventBubbleContent( overlay = { TimelineEventTimestampView( event = event, + onShieldClick = onShieldClick, modifier = Modifier .padding(horizontal = 8.dp, vertical = 4.dp) ) @@ -460,6 +470,7 @@ private fun MessageEventBubbleContent( content {} TimelineEventTimestampView( event = event, + onShieldClick = onShieldClick, modifier = Modifier .align(Alignment.End) .padding(horizontal = 8.dp, vertical = 4.dp) @@ -507,6 +518,7 @@ private fun MessageEventBubbleContent( val contentWithTimestamp = @Composable { WithTimestampLayout( timestampPosition = timestampPosition, + onShieldClick = onShieldClick, canShrinkContent = canShrinkContent, modifier = timestampLayoutModifier, ) { onContentLayoutChange -> @@ -519,6 +531,7 @@ private fun MessageEventBubbleContent( ) } } + val inReplyTo = @Composable { inReplyTo: InReplyToDetails -> val topPadding = if (showThreadDecoration) 0.dp else 8.dp val inReplyToModifier = Modifier 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..828060fd1f --- /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) + +internal 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/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index 3992c3304e..bbf00a83a9 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 @@ -86,6 +86,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 5eea9851ae..208a6b07dc 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 @@ -83,6 +84,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/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/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( 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( 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/EventTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt index 4a8f330c5c..cc2ad701fe 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 @@ -39,6 +39,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..9092817569 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt @@ -0,0 +1,46 @@ +/* + * 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 + +import androidx.compose.runtime.Immutable + +@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 +} + +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 + } 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 2ae0a3e909..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 @@ -23,6 +23,7 @@ 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 @@ -31,6 +32,8 @@ 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 @@ -56,7 +59,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(), ) } } @@ -129,3 +133,24 @@ private fun RustEventItemOrigin.map(): TimelineItemEventOrigin { RustEventItemOrigin.PAGINATION -> TimelineItemEventOrigin.PAGINATION } } + +private fun ShieldState?.map(): MessageShield? { + 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, + 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/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 f9e9efb210..3712fb56af 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 @@ -58,6 +59,7 @@ fun anEventTimelineItem( timestamp: Long = 0L, content: EventContent = aProfileChangeMessageContent(), debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), + messageShield: MessageShield? = null, ) = EventTimelineItem( eventId = eventId, transactionId = transactionId, @@ -75,6 +77,7 @@ fun anEventTimelineItem( content = content, debugInfo = debugInfo, origin = null, + messageShield = messageShield, ) fun aProfileTimelineDetails( 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..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 @@ -120,6 +120,7 @@ class KonsistPreviewTest { "TextComposerVoicePreview", "TimelineImageWithCaptionRowPreview", "TimelineItemEventRowForDirectRoomPreview", + "TimelineItemEventRowShieldPreview", "TimelineItemEventRowTimestampPreview", "TimelineItemEventRowWithManyReactionsPreview", "TimelineItemEventRowWithRRPreview", @@ -128,6 +129,7 @@ class KonsistPreviewTest { "TimelineItemGroupedEventsRowContentExpandedPreview", "TimelineItemVoiceViewUnifiedPreview", "TimelineVideoWithCaptionRowPreview", + "TimelineViewMessageShieldPreview", "UserAvatarColorsPreview", ) .assertTrue( 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..aa89a718dc --- /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: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 new file mode 100644 index 0000000000..9b49043ac6 --- /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: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 new file mode 100644 index 0000000000..b0b3fd1a0b --- /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:6d37081422eb655aaaa58834912625efd07f61eddcb7117f32b8edb9512969ab +size 5052 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..8759060547 --- /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:6dcce84695624b516035b2a3fe33ad71bf0b278a769ac013ad7c9b4feae958c5 +size 5028 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..0186b92f5f --- /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: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 new file mode 100644 index 0000000000..6660328620 --- /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: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 new file mode 100644 index 0000000000..e78b401753 --- /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:99acfb92911fbc7b60126cdadbd5b373f9692258d4f6c59a8167425e7b5603b5 +size 30870 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..ce1c266e0a --- /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:7348bb4cc9cde7eda1c7f65ce5f997838add5d8a84974b13bcf6685b15c80f1a +size 31136 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/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 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