Browse Source

Iterate on shield mapping and rendering

Also handle click on the timeline and information displayed on long click.
pull/3240/head
Benoit Marty 1 month ago committed by Benoit Marty
parent
commit
faf1e7da9f
  1. 11
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt
  2. 10
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt
  3. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt
  4. 5
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
  5. 3
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt
  6. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt
  7. 19
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
  8. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt
  9. 25
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldPosition.kt
  10. 103
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt
  11. 29
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt
  12. 7
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventForTimestampViewProvider.kt
  13. 164
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
  14. 78
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowShieldPreview.kt
  15. 7
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt
  16. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt
  17. 12
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt
  18. 5
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MessageEventFixtures.kt
  19. 3
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt
  20. 3
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/RedactedVoiceMessageManagerTest.kt
  21. 92
      libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/AlertDialog.kt
  22. 36
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt
  23. 22
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt
  24. 1
      tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt

11
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 @@ -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<ActionListState> { @@ -121,6 +122,16 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
actions = aTimelineItemPollActionList(),
),
),
anActionListState().copy(
target = ActionListState.Target.Success(
event = aTimelineItemEvent().copy(
reactionsState = reactionsState,
messageShield = MessageShield.UnknownDevice(isCritical = true)
),
displayEmojiReactions = true,
actions = aTimelineItemActionList(),
)
),
)
}
}

10
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 @@ -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( @@ -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()
}
}

4
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 @@ -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 { @@ -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.
*/

5
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 @@ -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( @@ -97,6 +98,7 @@ class TimelinePresenter @AssistedInject constructor(
val prevMostRecentItemId = rememberSaveable { mutableStateOf<String?>(null) }
val newEventState = remember { mutableStateOf(NewEventState.None) }
val messageShield: MutableState<MessageShield?> = 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( @@ -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( @@ -226,6 +230,7 @@ class TimelinePresenter @AssistedInject constructor(
newEventState = newEventState.value,
isLive = isLive,
focusRequestState = focusRequestState.value,
messageShield = messageShield.value,
eventSink = { handleEvents(it) }
)
}

3
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt

@ -20,6 +20,7 @@ import androidx.compose.runtime.Immutable @@ -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( @@ -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 }

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt

@ -51,6 +51,7 @@ fun aTimelineState( @@ -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<TimelineItem.Event>().getOrNull(focusedEventIndex)?.eventId
@ -66,6 +67,7 @@ fun aTimelineState( @@ -66,6 +67,7 @@ fun aTimelineState(
newEventState = NewEventState.None,
isLive = isLive,
focusRequestState = focusRequestState,
messageShield = messageShield,
eventSink = eventSink,
)
}

19
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 @@ -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 @@ -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( @@ -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( @@ -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( @@ -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

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/ATimelineItemEventRow.kt

@ -37,6 +37,7 @@ internal fun ATimelineItemEventRow( @@ -37,6 +37,7 @@ internal fun ATimelineItemEventRow(
isHighlighted = isHighlighted,
onClick = {},
onLongClick = {},
onShieldClick = {},
onUserDataClick = {},
onLinkClick = {},
inReplyToClick = {},

25
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldPosition.kt

@ -1,25 +0,0 @@ @@ -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()
}

103
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageShieldView.kt

@ -16,19 +16,18 @@ @@ -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 @@ -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 { @@ -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)
)
}
}

29
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt

@ -16,6 +16,7 @@ @@ -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 @@ -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( @@ -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 {

7
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 @@ -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<TimelineItem.Event> {
override val values: Sequence<TimelineItem.Event>
@ -37,5 +38,11 @@ class TimelineItemEventForTimestampViewProvider : PreviewParameterProvider<Timel @@ -37,5 +38,11 @@ class TimelineItemEventForTimestampViewProvider : PreviewParameterProvider<Timel
localSendState = LocalEventSendState.SendingFailed.Unrecoverable("AN_ERROR"),
content = aTimelineItemTextContent().copy(isEdited = true),
),
aTimelineItemEvent().copy(
messageShield = MessageShield.AuthenticityNotGuaranteed(isCritical = false),
),
aTimelineItemEvent().copy(
messageShield = MessageShield.UnknownDevice(isCritical = true),
),
)
}

164
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt

@ -17,7 +17,6 @@ @@ -17,7 +17,6 @@
package io.element.android.features.messages.impl.timeline.components
import android.annotation.SuppressLint
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
@ -37,7 +36,6 @@ import androidx.compose.foundation.layout.wrapContentHeight @@ -37,7 +36,6 @@ import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
@ -52,7 +50,6 @@ import androidx.compose.ui.semantics.contentDescription @@ -52,7 +50,6 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.invisibleToUser
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
@ -78,8 +75,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt @@ -78,8 +75,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
import io.element.android.features.messages.impl.timeline.model.event.aGreyShield
import io.element.android.features.messages.impl.timeline.model.event.aRedShield
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
@ -95,6 +90,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon @@ -95,6 +90,7 @@ 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.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.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToView
@ -123,6 +119,7 @@ fun TimelineItemEventRow( @@ -123,6 +119,7 @@ fun TimelineItemEventRow(
isHighlighted: Boolean,
onClick: () -> Unit,
onLongClick: () -> Unit,
onShieldClick: (MessageShield) -> Unit,
onLinkClick: (String) -> Unit,
onUserDataClick: (UserId) -> Unit,
inReplyToClick: (EventId) -> Unit,
@ -185,6 +182,7 @@ fun TimelineItemEventRow( @@ -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( @@ -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( @@ -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( @@ -281,7 +281,6 @@ private fun TimelineItemEventRowContent(
val (
sender,
message,
shield,
reactions,
) = createRefs()
@ -326,6 +325,7 @@ private fun TimelineItemEventRowContent( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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 { @@ -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)
}
}

78
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowShieldPreview.kt

@ -0,0 +1,78 @@ @@ -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)

7
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 @@ -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( @@ -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( @@ -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( @@ -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( @@ -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 @@ -168,6 +173,7 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi
isLastOutgoingMessage = false,
onClick = {},
onLongClick = {},
onShieldClick = {},
inReplyToClick = {},
onUserDataClick = {},
onLinkClick = {},
@ -192,6 +198,7 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi @@ -192,6 +198,7 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi
isLastOutgoingMessage = false,
onClick = {},
onLongClick = {},
onShieldClick = {},
inReplyToClick = {},
onUserDataClick = {},
onLinkClick = {},

4
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 @@ -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( @@ -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( @@ -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( @@ -132,6 +135,7 @@ internal fun TimelineItemRow(
focusedEventId = focusedEventId,
onClick = onClick,
onLongClick = onLongClick,
onShieldClick = onShieldClick,
inReplyToClick = inReplyToClick,
onUserDataClick = onUserDataClick,
onLinkClick = onLinkClick,

12
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 @@ -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<TimelineItemEventContent> {
@ -104,13 +102,3 @@ fun aTimelineItemStateEventContent( @@ -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
)

5
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 @@ -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( @@ -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( @@ -66,5 +68,6 @@ internal fun aMessageEvent(
inReplyTo = inReplyTo,
debugInfo = debugInfo,
isThreaded = isThreaded,
origin = null
origin = null,
messageShield = messageShield,
)

3
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt

@ -50,7 +50,8 @@ class TimelineItemGrouperTest { @@ -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"))

3
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<MatrixTimelineItem>( @@ -101,7 +101,8 @@ fun aRedactedMatrixTimeline(eventId: EventId) = listOf<MatrixTimelineItem>(
originalJson = null,
latestEditedJson = null
),
origin = null
origin = null,
messageShield = null,
),
)
)

92
libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/dialogs/AlertDialog.kt

@ -0,0 +1,92 @@ @@ -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 = {},
)
}

36
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageShield.kt

@ -16,12 +16,32 @@ @@ -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
}
}

22
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 @@ -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 { @@ -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)
}
}

1
tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt

@ -120,6 +120,7 @@ class KonsistPreviewTest { @@ -120,6 +120,7 @@ class KonsistPreviewTest {
"TextComposerVoicePreview",
"TimelineImageWithCaptionRowPreview",
"TimelineItemEventRowForDirectRoomPreview",
"TimelineItemEventRowShieldPreview",
"TimelineItemEventRowTimestampPreview",
"TimelineItemEventRowWithManyReactionsPreview",
"TimelineItemEventRowWithRRPreview",

Loading…
Cancel
Save