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.UnknownDevice(true) + ) MessageShieldView( - shield = MessageShield( - message = "The authenticity of this encrypted message can't be guaranteed on this device.", - color = ShieldColor.GREY - ) + shield = MessageShield.UnverifiedIdentity(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.AuthenticityNotGuaranteed(false) ) MessageShieldView( - shield = MessageShield( - message = "Encrypted by a device not verified by its owner.", - color = ShieldColor.RED - ) + shield = MessageShield.UnsignedDevice(false) ) MessageShieldView( - shield = MessageShield( - message = "Encrypted by an unknown or deleted device.", - color = ShieldColor.RED - ) + 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, -) - -enum class ShieldColor { - RED, - GREY +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 +} + +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",