Iterate on shield mapping and rendering
Also handle click on the timeline and information displayed on long click.
This commit is contained in:
parent
5dceba8146
commit
34268a30ea
24 changed files with 399 additions and 245 deletions
|
|
@ -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.aTimelineItemPollContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent
|
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.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.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
|
@ -121,6 +122,16 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||||
actions = aTimelineItemPollActionList(),
|
actions = aTimelineItemPollActionList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
anActionListState().copy(
|
||||||
|
target = ActionListState.Target.Success(
|
||||||
|
event = aTimelineItemEvent().copy(
|
||||||
|
reactionsState = reactionsState,
|
||||||
|
messageShield = MessageShield.UnknownDevice(isCritical = true)
|
||||||
|
),
|
||||||
|
displayEmojiReactions = true,
|
||||||
|
actions = aTimelineItemActionList(),
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
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.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.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemCallNotifyContent
|
||||||
|
|
@ -181,7 +182,14 @@ private fun SheetContent(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.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()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
sealed interface TimelineEvents {
|
sealed interface TimelineEvents {
|
||||||
|
|
@ -27,6 +28,9 @@ sealed interface TimelineEvents {
|
||||||
data object OnFocusEventRender : TimelineEvents
|
data object OnFocusEventRender : TimelineEvents
|
||||||
data object JumpToLive : TimelineEvents
|
data object JumpToLive : TimelineEvents
|
||||||
|
|
||||||
|
data class ShowShieldDialog(val messageShield: MessageShield) : TimelineEvents
|
||||||
|
data object HideShieldDialog : TimelineEvents
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events coming from a timeline item.
|
* Events coming from a timeline item.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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.isDm
|
||||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
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.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.api.timeline.item.event.TimelineItemEventOrigin
|
||||||
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
|
||||||
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
|
||||||
|
|
@ -97,6 +98,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
val prevMostRecentItemId = rememberSaveable { mutableStateOf<String?>(null) }
|
val prevMostRecentItemId = rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
val newEventState = remember { mutableStateOf(NewEventState.None) }
|
val newEventState = remember { mutableStateOf(NewEventState.None) }
|
||||||
|
val messageShield: MutableState<MessageShield?> = remember { mutableStateOf(null) }
|
||||||
|
|
||||||
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true)
|
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true)
|
||||||
val renderReadReceipts by sessionPreferencesStore.isRenderReadReceiptsEnabled().collectAsState(initial = true)
|
val renderReadReceipts by sessionPreferencesStore.isRenderReadReceiptsEnabled().collectAsState(initial = true)
|
||||||
|
|
@ -151,6 +153,8 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
is TimelineEvents.JumpToLive -> {
|
is TimelineEvents.JumpToLive -> {
|
||||||
timelineController.focusOnLive()
|
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,
|
newEventState = newEventState.value,
|
||||||
isLive = isLive,
|
isLive = isLive,
|
||||||
focusRequestState = focusRequestState.value,
|
focusRequestState = focusRequestState.value,
|
||||||
|
messageShield = messageShield.value,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = { handleEvents(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
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.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
|
@ -31,6 +32,8 @@ data class TimelineState(
|
||||||
val newEventState: NewEventState,
|
val newEventState: NewEventState,
|
||||||
val isLive: Boolean,
|
val isLive: Boolean,
|
||||||
val focusRequestState: FocusRequestState,
|
val focusRequestState: FocusRequestState,
|
||||||
|
// If not null, info will be rendered in a dialog
|
||||||
|
val messageShield: MessageShield?,
|
||||||
val eventSink: (TimelineEvents) -> Unit,
|
val eventSink: (TimelineEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
val hasAnyEvent = timelineItems.any { it is TimelineItem.Event }
|
val hasAnyEvent = timelineItems.any { it is TimelineItem.Event }
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ fun aTimelineState(
|
||||||
timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(),
|
timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(),
|
||||||
focusedEventIndex: Int = -1,
|
focusedEventIndex: Int = -1,
|
||||||
isLive: Boolean = true,
|
isLive: Boolean = true,
|
||||||
|
messageShield: MessageShield? = null,
|
||||||
eventSink: (TimelineEvents) -> Unit = {},
|
eventSink: (TimelineEvents) -> Unit = {},
|
||||||
): TimelineState {
|
): TimelineState {
|
||||||
val focusedEventId = timelineItems.filterIsInstance<TimelineItem.Event>().getOrNull(focusedEventIndex)?.eventId
|
val focusedEventId = timelineItems.filterIsInstance<TimelineItem.Event>().getOrNull(focusedEventIndex)?.eventId
|
||||||
|
|
@ -66,6 +67,7 @@ fun aTimelineState(
|
||||||
newEventState = NewEventState.None,
|
newEventState = NewEventState.None,
|
||||||
isLive = isLive,
|
isLive = isLive,
|
||||||
focusRequestState = focusRequestState,
|
focusRequestState = focusRequestState,
|
||||||
|
messageShield = messageShield,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
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.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.LocalTimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.focus.FocusRequestStateView
|
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.TypingNotificationState
|
||||||
import io.element.android.features.messages.impl.typing.TypingNotificationView
|
import io.element.android.features.messages.impl.typing.TypingNotificationView
|
||||||
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
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.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
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.FloatingActionButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
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.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
@ -124,6 +127,10 @@ fun TimelineView(
|
||||||
state.eventSink(TimelineEvents.FocusOnEvent(eventId))
|
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
|
// Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms
|
||||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
|
|
@ -154,6 +161,7 @@ fun TimelineView(
|
||||||
focusedEventId = state.focusedEventId,
|
focusedEventId = state.focusedEventId,
|
||||||
onClick = onMessageClick,
|
onClick = onMessageClick,
|
||||||
onLongClick = onMessageLongClick,
|
onLongClick = onMessageLongClick,
|
||||||
|
onShieldClick = ::onShieldClick,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
inReplyToClick = ::inReplyToClick,
|
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
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ internal fun ATimelineItemEventRow(
|
||||||
isHighlighted = isHighlighted,
|
isHighlighted = isHighlighted,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
|
onShieldClick = {},
|
||||||
onUserDataClick = {},
|
onUserDataClick = {},
|
||||||
onLinkClick = {},
|
onLinkClick = {},
|
||||||
inReplyToClick = {},
|
inReplyToClick = {},
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
@ -16,19 +16,18 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.components
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
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.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
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.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.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
|
@Composable
|
||||||
internal fun MessageShieldView(
|
internal fun MessageShieldView(
|
||||||
isMine: Boolean = false,
|
|
||||||
shield: MessageShield,
|
shield: MessageShield,
|
||||||
modifier: Modifier = Modifier
|
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(
|
Row(
|
||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.background(backgroundBubbleColor, RoundedCornerShape(8.dp))
|
|
||||||
.border(1.dp, borderColor, RoundedCornerShape(8.dp))
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = shield.toIcon(),
|
imageVector = shield.toIcon(),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(15.dp),
|
modifier = Modifier.size(15.dp),
|
||||||
tint = iconColor,
|
tint = shield.toIconColor(),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(4.dp))
|
Spacer(modifier = Modifier.size(4.dp))
|
||||||
val textColor = if (shield.color == ShieldColor.RED) ElementTheme.colors.textCriticalPrimary else ElementTheme.colors.textSecondary
|
|
||||||
Text(
|
Text(
|
||||||
text = shield.message,
|
text = shield.toText(),
|
||||||
style = ElementTheme.typography.fontBodyXsRegular,
|
style = ElementTheme.typography.fontBodySmMedium,
|
||||||
color = textColor
|
color = shield.toTextColor()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageShield.toIcon(): ImageVector {
|
internal fun MessageShield.toIconColor(): Color {
|
||||||
return when (this.color) {
|
return when (isCritical()) {
|
||||||
ShieldColor.RED -> CompoundIcons.Error()
|
true -> ElementTheme.colors.iconCriticalPrimary
|
||||||
ShieldColor.GREY -> CompoundIcons.InfoSolid()
|
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
|
@Composable
|
||||||
internal fun MessageShieldViewPreview() {
|
internal fun MessageShieldViewPreview() {
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
MessageShieldView(
|
MessageShieldView(
|
||||||
shield = MessageShield(
|
shield = MessageShield.UnknownDevice(true)
|
||||||
message = "The authenticity of this encrypted message can't be guaranteed on this device.",
|
|
||||||
color = ShieldColor.GREY
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
MessageShieldView(
|
MessageShieldView(
|
||||||
isMine = true,
|
shield = MessageShield.UnverifiedIdentity(true)
|
||||||
shield = MessageShield(
|
|
||||||
message = "The authenticity of this encrypted message can't be guaranteed on this device.",
|
|
||||||
color = ShieldColor.GREY
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
MessageShieldView(
|
MessageShieldView(
|
||||||
shield = MessageShield(
|
shield = MessageShield.AuthenticityNotGuaranteed(false)
|
||||||
message = "Encrypted by a device not verified by its owner.",
|
|
||||||
color = ShieldColor.RED
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
MessageShieldView(
|
MessageShieldView(
|
||||||
shield = MessageShield(
|
shield = MessageShield.UnsignedDevice(false)
|
||||||
message = "Encrypted by an unknown or deleted device.",
|
)
|
||||||
color = ShieldColor.RED
|
MessageShieldView(
|
||||||
)
|
shield = MessageShield.SentInClear(false)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.components
|
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.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
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.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.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
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.Icon
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
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.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
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineEventTimestampView(
|
fun TimelineEventTimestampView(
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
|
onShieldClick: (MessageShield) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val formattedTime = event.sentTime
|
val formattedTime = event.sentTime
|
||||||
val hasUnrecoverableError = event.localSendState is LocalEventSendState.SendingFailed.Unrecoverable
|
val hasUnrecoverableError = event.localSendState is LocalEventSendState.SendingFailed.Unrecoverable
|
||||||
|
val hasEncryptionCritical = event.messageShield?.isCritical().orFalse()
|
||||||
val isMessageEdited = event.content.isEdited()
|
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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing))
|
.padding(PaddingValues(start = TimelineEventTimestampViewDefaults.spacing))
|
||||||
.then(modifier),
|
.then(modifier),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
if (isMessageEdited) {
|
if (isMessageEdited) {
|
||||||
|
|
@ -77,13 +83,28 @@ fun TimelineEventTimestampView(
|
||||||
modifier = Modifier.size(15.dp, 18.dp),
|
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
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TimelineEventTimestampViewPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) = ElementPreview {
|
internal fun TimelineEventTimestampViewPreview(@PreviewParameter(TimelineItemEventForTimestampViewProvider::class) event: TimelineItem.Event) = ElementPreview {
|
||||||
TimelineEventTimestampView(event = event)
|
TimelineEventTimestampView(
|
||||||
|
event = event,
|
||||||
|
onShieldClick = {},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
object TimelineEventTimestampViewDefaults {
|
object TimelineEventTimestampViewDefaults {
|
||||||
|
|
|
||||||
|
|
@ -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.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
|
|
||||||
class TimelineItemEventForTimestampViewProvider : PreviewParameterProvider<TimelineItem.Event> {
|
class TimelineItemEventForTimestampViewProvider : PreviewParameterProvider<TimelineItem.Event> {
|
||||||
override val values: Sequence<TimelineItem.Event>
|
override val values: Sequence<TimelineItem.Event>
|
||||||
|
|
@ -37,5 +38,11 @@ class TimelineItemEventForTimestampViewProvider : PreviewParameterProvider<Timel
|
||||||
localSendState = LocalEventSendState.SendingFailed.Unrecoverable("AN_ERROR"),
|
localSendState = LocalEventSendState.SendingFailed.Unrecoverable("AN_ERROR"),
|
||||||
content = aTimelineItemTextContent().copy(isEdited = true),
|
content = aTimelineItemTextContent().copy(isEdited = true),
|
||||||
),
|
),
|
||||||
|
aTimelineItemEvent().copy(
|
||||||
|
messageShield = MessageShield.AuthenticityNotGuaranteed(isCritical = false),
|
||||||
|
),
|
||||||
|
aTimelineItemEvent().copy(
|
||||||
|
messageShield = MessageShield.UnknownDevice(isCritical = true),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
package io.element.android.features.messages.impl.timeline.components
|
package io.element.android.features.messages.impl.timeline.components
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.res.Configuration
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
|
@ -37,7 +36,6 @@ import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|
@ -52,7 +50,6 @@ import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.invisibleToUser
|
import androidx.compose.ui.semantics.invisibleToUser
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.semantics.testTag
|
import androidx.compose.ui.semantics.testTag
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -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.TimelineItemStickerContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aGreyShield
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aRedShield
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
|
||||||
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
||||||
|
|
@ -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.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.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.api.timeline.item.event.ProfileTimelineDetails
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
|
||||||
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToView
|
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToView
|
||||||
|
|
@ -123,6 +119,7 @@ fun TimelineItemEventRow(
|
||||||
isHighlighted: Boolean,
|
isHighlighted: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
|
onShieldClick: (MessageShield) -> Unit,
|
||||||
onLinkClick: (String) -> Unit,
|
onLinkClick: (String) -> Unit,
|
||||||
onUserDataClick: (UserId) -> Unit,
|
onUserDataClick: (UserId) -> Unit,
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
|
|
@ -185,6 +182,7 @@ fun TimelineItemEventRow(
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
|
onShieldClick = onShieldClick,
|
||||||
inReplyToClick = ::inReplyToClick,
|
inReplyToClick = ::inReplyToClick,
|
||||||
onUserDataClick = ::onUserDataClick,
|
onUserDataClick = ::onUserDataClick,
|
||||||
onReactionClick = { emoji -> onReactionClick(emoji, event) },
|
onReactionClick = { emoji -> onReactionClick(emoji, event) },
|
||||||
|
|
@ -203,6 +201,7 @@ fun TimelineItemEventRow(
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
|
onShieldClick = onShieldClick,
|
||||||
inReplyToClick = ::inReplyToClick,
|
inReplyToClick = ::inReplyToClick,
|
||||||
onUserDataClick = ::onUserDataClick,
|
onUserDataClick = ::onUserDataClick,
|
||||||
onReactionClick = { emoji -> onReactionClick(emoji, event) },
|
onReactionClick = { emoji -> onReactionClick(emoji, event) },
|
||||||
|
|
@ -258,6 +257,7 @@ private fun TimelineItemEventRowContent(
|
||||||
interactionSource: MutableInteractionSource,
|
interactionSource: MutableInteractionSource,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
|
onShieldClick: (MessageShield) -> Unit,
|
||||||
inReplyToClick: () -> Unit,
|
inReplyToClick: () -> Unit,
|
||||||
onUserDataClick: () -> Unit,
|
onUserDataClick: () -> Unit,
|
||||||
onReactionClick: (emoji: String) -> Unit,
|
onReactionClick: (emoji: String) -> Unit,
|
||||||
|
|
@ -281,7 +281,6 @@ private fun TimelineItemEventRowContent(
|
||||||
val (
|
val (
|
||||||
sender,
|
sender,
|
||||||
message,
|
message,
|
||||||
shield,
|
|
||||||
reactions,
|
reactions,
|
||||||
) = createRefs()
|
) = createRefs()
|
||||||
|
|
||||||
|
|
@ -326,6 +325,7 @@ private fun TimelineItemEventRowContent(
|
||||||
) {
|
) {
|
||||||
MessageEventBubbleContent(
|
MessageEventBubbleContent(
|
||||||
event = event,
|
event = event,
|
||||||
|
onShieldClick = onShieldClick,
|
||||||
onMessageLongClick = onLongClick,
|
onMessageLongClick = onLongClick,
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
onLinkClick = onLinkClick,
|
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
|
// Reactions
|
||||||
if (event.reactionsState.reactions.isNotEmpty()) {
|
if (event.reactionsState.reactions.isNotEmpty()) {
|
||||||
TimelineItemReactionsView(
|
TimelineItemReactionsView(
|
||||||
|
|
@ -367,11 +344,7 @@ private fun TimelineItemEventRowContent(
|
||||||
onMoreReactionsClick = { onMoreReactionsClick(event) },
|
onMoreReactionsClick = { onMoreReactionsClick(event) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.constrainAs(reactions) {
|
.constrainAs(reactions) {
|
||||||
if (shieldPosition is MessageShieldPosition.OutOfBubble) {
|
top.linkTo(message.bottom, margin = (-4).dp)
|
||||||
top.linkTo(shield.bottom, margin = (-4).dp)
|
|
||||||
} else {
|
|
||||||
top.linkTo(message.bottom, margin = (-4).dp)
|
|
||||||
}
|
|
||||||
linkStartOrEnd(event)
|
linkStartOrEnd(event)
|
||||||
}
|
}
|
||||||
.zIndex(1f)
|
.zIndex(1f)
|
||||||
|
|
@ -413,6 +386,7 @@ private fun MessageSenderInformation(
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageEventBubbleContent(
|
private fun MessageEventBubbleContent(
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
|
onShieldClick: (MessageShield) -> Unit,
|
||||||
onMessageLongClick: () -> Unit,
|
onMessageLongClick: () -> Unit,
|
||||||
inReplyToClick: () -> Unit,
|
inReplyToClick: () -> Unit,
|
||||||
onLinkClick: (String) -> Unit,
|
onLinkClick: (String) -> Unit,
|
||||||
|
|
@ -453,6 +427,7 @@ private fun MessageEventBubbleContent(
|
||||||
@Composable
|
@Composable
|
||||||
fun WithTimestampLayout(
|
fun WithTimestampLayout(
|
||||||
timestampPosition: TimestampPosition,
|
timestampPosition: TimestampPosition,
|
||||||
|
onShieldClick: (MessageShield) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
canShrinkContent: Boolean = false,
|
canShrinkContent: Boolean = false,
|
||||||
content: @Composable (onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit) -> Unit,
|
content: @Composable (onContentLayoutChange: (ContentAvoidingLayoutData) -> Unit) -> Unit,
|
||||||
|
|
@ -463,6 +438,7 @@ private fun MessageEventBubbleContent(
|
||||||
content {}
|
content {}
|
||||||
TimelineEventTimestampView(
|
TimelineEventTimestampView(
|
||||||
event = event,
|
event = event,
|
||||||
|
onShieldClick = onShieldClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
// Outer padding
|
// Outer padding
|
||||||
.padding(horizontal = 4.dp, vertical = 4.dp)
|
.padding(horizontal = 4.dp, vertical = 4.dp)
|
||||||
|
|
@ -483,6 +459,7 @@ private fun MessageEventBubbleContent(
|
||||||
overlay = {
|
overlay = {
|
||||||
TimelineEventTimestampView(
|
TimelineEventTimestampView(
|
||||||
event = event,
|
event = event,
|
||||||
|
onShieldClick = onShieldClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
)
|
)
|
||||||
|
|
@ -493,6 +470,7 @@ private fun MessageEventBubbleContent(
|
||||||
content {}
|
content {}
|
||||||
TimelineEventTimestampView(
|
TimelineEventTimestampView(
|
||||||
event = event,
|
event = event,
|
||||||
|
onShieldClick = onShieldClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.End)
|
.align(Alignment.End)
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
|
@ -505,7 +483,6 @@ private fun MessageEventBubbleContent(
|
||||||
@Composable
|
@Composable
|
||||||
fun CommonLayout(
|
fun CommonLayout(
|
||||||
timestampPosition: TimestampPosition,
|
timestampPosition: TimestampPosition,
|
||||||
messageShieldPosition: MessageShieldPosition,
|
|
||||||
showThreadDecoration: Boolean,
|
showThreadDecoration: Boolean,
|
||||||
inReplyToDetails: InReplyToDetails?,
|
inReplyToDetails: InReplyToDetails?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -541,35 +518,20 @@ private fun MessageEventBubbleContent(
|
||||||
val contentWithTimestamp = @Composable {
|
val contentWithTimestamp = @Composable {
|
||||||
WithTimestampLayout(
|
WithTimestampLayout(
|
||||||
timestampPosition = timestampPosition,
|
timestampPosition = timestampPosition,
|
||||||
|
onShieldClick = onShieldClick,
|
||||||
canShrinkContent = canShrinkContent,
|
canShrinkContent = canShrinkContent,
|
||||||
modifier = timestampLayoutModifier,
|
modifier = timestampLayoutModifier,
|
||||||
) { onContentLayoutChange ->
|
) { onContentLayoutChange ->
|
||||||
|
TimelineItemEventContentView(
|
||||||
if (messageShieldPosition is MessageShieldPosition.InBubble) {
|
content = event.content,
|
||||||
Column {
|
onLinkClick = onLinkClick,
|
||||||
TimelineItemEventContentView(
|
eventSink = eventSink,
|
||||||
content = event.content,
|
onContentLayoutChange = onContentLayoutChange,
|
||||||
onLinkClick = onLinkClick,
|
modifier = contentModifier
|
||||||
eventSink = eventSink,
|
)
|
||||||
onContentLayoutChange = onContentLayoutChange,
|
|
||||||
modifier = contentModifier
|
|
||||||
)
|
|
||||||
MessageShieldView(
|
|
||||||
modifier = Modifier.padding(start = 8.dp, end = 8.dp),
|
|
||||||
shield = messageShieldPosition.messageShield,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TimelineItemEventContentView(
|
|
||||||
content = event.content,
|
|
||||||
onLinkClick = onLinkClick,
|
|
||||||
eventSink = eventSink,
|
|
||||||
onContentLayoutChange = onContentLayoutChange,
|
|
||||||
modifier = contentModifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val inReplyTo = @Composable { inReplyTo: InReplyToDetails ->
|
val inReplyTo = @Composable { inReplyTo: InReplyToDetails ->
|
||||||
val topPadding = if (showThreadDecoration) 0.dp else 8.dp
|
val topPadding = if (showThreadDecoration) 0.dp else 8.dp
|
||||||
val inReplyToModifier = Modifier
|
val inReplyToModifier = Modifier
|
||||||
|
|
@ -602,11 +564,9 @@ private fun MessageEventBubbleContent(
|
||||||
is TimelineItemPollContent -> TimestampPosition.Below
|
is TimelineItemPollContent -> TimestampPosition.Below
|
||||||
else -> TimestampPosition.Default
|
else -> TimestampPosition.Default
|
||||||
}
|
}
|
||||||
val messageShieldPosition = event.shieldPosition()
|
|
||||||
CommonLayout(
|
CommonLayout(
|
||||||
showThreadDecoration = event.isThreaded,
|
showThreadDecoration = event.isThreaded,
|
||||||
messageShieldPosition = messageShieldPosition,
|
timestampPosition = timestampPosition,
|
||||||
timestampPosition = if (messageShieldPosition is MessageShieldPosition.InBubble) TimestampPosition.Below else timestampPosition,
|
|
||||||
inReplyToDetails = event.inReplyTo,
|
inReplyToDetails = event.inReplyTo,
|
||||||
canShrinkContent = event.content is TimelineItemVoiceContent,
|
canShrinkContent = event.content is TimelineItemVoiceContent,
|
||||||
modifier = bubbleModifier.semantics(mergeDescendants = true) {
|
modifier = bubbleModifier.semantics(mergeDescendants = true) {
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemGroupedEventsRow(
|
fun TimelineItemGroupedEventsRow(
|
||||||
|
|
@ -46,6 +47,7 @@ fun TimelineItemGroupedEventsRow(
|
||||||
focusedEventId: EventId?,
|
focusedEventId: EventId?,
|
||||||
onClick: (TimelineItem.Event) -> Unit,
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
|
onShieldClick: (MessageShield) -> Unit,
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
onUserDataClick: (UserId) -> Unit,
|
onUserDataClick: (UserId) -> Unit,
|
||||||
onLinkClick: (String) -> Unit,
|
onLinkClick: (String) -> Unit,
|
||||||
|
|
@ -72,6 +74,7 @@ fun TimelineItemGroupedEventsRow(
|
||||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
|
onShieldClick = onShieldClick,
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
|
|
@ -95,6 +98,7 @@ private fun TimelineItemGroupedEventsRowContent(
|
||||||
isLastOutgoingMessage: Boolean,
|
isLastOutgoingMessage: Boolean,
|
||||||
onClick: (TimelineItem.Event) -> Unit,
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
|
onShieldClick: (MessageShield) -> Unit,
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
onUserDataClick: (UserId) -> Unit,
|
onUserDataClick: (UserId) -> Unit,
|
||||||
onLinkClick: (String) -> Unit,
|
onLinkClick: (String) -> Unit,
|
||||||
|
|
@ -127,6 +131,7 @@ private fun TimelineItemGroupedEventsRowContent(
|
||||||
focusedEventId = focusedEventId,
|
focusedEventId = focusedEventId,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
|
onShieldClick = onShieldClick,
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
|
|
@ -168,6 +173,7 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi
|
||||||
isLastOutgoingMessage = false,
|
isLastOutgoingMessage = false,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
|
onShieldClick = {},
|
||||||
inReplyToClick = {},
|
inReplyToClick = {},
|
||||||
onUserDataClick = {},
|
onUserDataClick = {},
|
||||||
onLinkClick = {},
|
onLinkClick = {},
|
||||||
|
|
@ -192,6 +198,7 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi
|
||||||
isLastOutgoingMessage = false,
|
isLastOutgoingMessage = false,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
|
onShieldClick = {},
|
||||||
inReplyToClick = {},
|
inReplyToClick = {},
|
||||||
onUserDataClick = {},
|
onUserDataClick = {},
|
||||||
onLinkClick = {},
|
onLinkClick = {},
|
||||||
|
|
|
||||||
|
|
@ -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.designsystem.theme.highlightedMessageBackgroundColor
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
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.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TimelineItemRow(
|
internal fun TimelineItemRow(
|
||||||
|
|
@ -49,6 +50,7 @@ internal fun TimelineItemRow(
|
||||||
onLinkClick: (String) -> Unit,
|
onLinkClick: (String) -> Unit,
|
||||||
onClick: (TimelineItem.Event) -> Unit,
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
|
onShieldClick: (MessageShield) -> Unit,
|
||||||
inReplyToClick: (EventId) -> Unit,
|
inReplyToClick: (EventId) -> Unit,
|
||||||
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
|
|
@ -110,6 +112,7 @@ internal fun TimelineItemRow(
|
||||||
isHighlighted = timelineItem.isEvent(focusedEventId),
|
isHighlighted = timelineItem.isEvent(focusedEventId),
|
||||||
onClick = { onClick(timelineItem) },
|
onClick = { onClick(timelineItem) },
|
||||||
onLongClick = { onLongClick(timelineItem) },
|
onLongClick = { onLongClick(timelineItem) },
|
||||||
|
onShieldClick = onShieldClick,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
|
|
@ -132,6 +135,7 @@ internal fun TimelineItemRow(
|
||||||
focusedEventId = focusedEventId,
|
focusedEventId = focusedEventId,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
|
onShieldClick = onShieldClick,
|
||||||
inReplyToClick = inReplyToClick,
|
inReplyToClick = inReplyToClick,
|
||||||
onUserDataClick = onUserDataClick,
|
onUserDataClick = onUserDataClick,
|
||||||
onLinkClick = onLinkClick,
|
onLinkClick = onLinkClick,
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,6 @@ import android.text.style.StyleSpan
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import androidx.core.text.buildSpannedString
|
import androidx.core.text.buildSpannedString
|
||||||
import androidx.core.text.inSpans
|
import androidx.core.text.inSpans
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ShieldColor
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
|
||||||
|
|
||||||
class TimelineItemEventContentProvider : PreviewParameterProvider<TimelineItemEventContent> {
|
class TimelineItemEventContentProvider : PreviewParameterProvider<TimelineItemEventContent> {
|
||||||
|
|
@ -104,13 +102,3 @@ fun aTimelineItemStateEventContent(
|
||||||
) = TimelineItemStateEventContent(
|
) = TimelineItemStateEventContent(
|
||||||
body = body,
|
body = body,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aGreyShield() = MessageShield(
|
|
||||||
message = "The authenticity of this encrypted message can't be guaranteed on this device.",
|
|
||||||
color = ShieldColor.GREY
|
|
||||||
)
|
|
||||||
|
|
||||||
fun aRedShield() = MessageShield(
|
|
||||||
message = "Encrypted by a device not verified by its owner.",
|
|
||||||
color = ShieldColor.RED
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -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.core.TransactionId
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
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_MESSAGE
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
|
|
@ -48,6 +49,7 @@ internal fun aMessageEvent(
|
||||||
isThreaded: Boolean = false,
|
isThreaded: Boolean = false,
|
||||||
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
|
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
|
||||||
sendState: LocalEventSendState = LocalEventSendState.Sent(AN_EVENT_ID),
|
sendState: LocalEventSendState = LocalEventSendState.Sent(AN_EVENT_ID),
|
||||||
|
messageShield: MessageShield? = null,
|
||||||
) = TimelineItem.Event(
|
) = TimelineItem.Event(
|
||||||
id = eventId?.value.orEmpty(),
|
id = eventId?.value.orEmpty(),
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
|
|
@ -66,5 +68,6 @@ internal fun aMessageEvent(
|
||||||
inReplyTo = inReplyTo,
|
inReplyTo = inReplyTo,
|
||||||
debugInfo = debugInfo,
|
debugInfo = debugInfo,
|
||||||
isThreaded = isThreaded,
|
isThreaded = isThreaded,
|
||||||
origin = null
|
origin = null,
|
||||||
|
messageShield = messageShield,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,8 @@ class TimelineItemGrouperTest {
|
||||||
inReplyTo = null,
|
inReplyTo = null,
|
||||||
isThreaded = false,
|
isThreaded = false,
|
||||||
debugInfo = aTimelineItemDebugInfo(),
|
debugInfo = aTimelineItemDebugInfo(),
|
||||||
origin = null
|
origin = null,
|
||||||
|
messageShield = null,
|
||||||
)
|
)
|
||||||
private val aNonGroupableItem = aMessageEvent()
|
private val aNonGroupableItem = aMessageEvent()
|
||||||
private val aNonGroupableItemNoEvent = TimelineItem.Virtual("virtual", aTimelineItemDaySeparatorModel("Today"))
|
private val aNonGroupableItemNoEvent = TimelineItem.Virtual("virtual", aTimelineItemDaySeparatorModel("Today"))
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,8 @@ fun aRedactedMatrixTimeline(eventId: EventId) = listOf<MatrixTimelineItem>(
|
||||||
originalJson = null,
|
originalJson = null,
|
||||||
latestEditedJson = null
|
latestEditedJson = null
|
||||||
),
|
),
|
||||||
origin = null
|
origin = null,
|
||||||
|
messageShield = null,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -16,12 +16,32 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.timeline.item.event
|
package io.element.android.libraries.matrix.api.timeline.item.event
|
||||||
|
|
||||||
data class MessageShield(
|
import androidx.compose.runtime.Immutable
|
||||||
val message: String,
|
|
||||||
val color: ShieldColor,
|
|
||||||
)
|
|
||||||
|
|
||||||
enum class ShieldColor {
|
@Immutable
|
||||||
RED,
|
sealed interface MessageShield {
|
||||||
GREY
|
/** 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.ProfileTimelineDetails
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
|
import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
|
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.ShieldColor
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import org.matrix.rustcomponents.sdk.Reaction
|
import org.matrix.rustcomponents.sdk.Reaction
|
||||||
import org.matrix.rustcomponents.sdk.ShieldState
|
import org.matrix.rustcomponents.sdk.ShieldState
|
||||||
|
import uniffi.matrix_sdk_common.ShieldStateCode
|
||||||
import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState
|
import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState
|
||||||
import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem
|
import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem
|
||||||
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo
|
import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo
|
||||||
|
|
@ -135,10 +135,22 @@ private fun RustEventItemOrigin.map(): TimelineItemEventOrigin {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ShieldState?.map(): MessageShield? {
|
private fun ShieldState?.map(): MessageShield? {
|
||||||
return when (this) {
|
this ?: return null
|
||||||
is ShieldState.Grey -> MessageShield(message = this.message, color = ShieldColor.GREY)
|
val shieldStateCode = when (this) {
|
||||||
is ShieldState.Red -> MessageShield(message = this.message, color = ShieldColor.RED)
|
is ShieldState.Grey -> code
|
||||||
|
is ShieldState.Red -> code
|
||||||
|
ShieldState.None -> null
|
||||||
|
} ?: return null
|
||||||
|
val isCritical = when (this) {
|
||||||
ShieldState.None,
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ class KonsistPreviewTest {
|
||||||
"TextComposerVoicePreview",
|
"TextComposerVoicePreview",
|
||||||
"TimelineImageWithCaptionRowPreview",
|
"TimelineImageWithCaptionRowPreview",
|
||||||
"TimelineItemEventRowForDirectRoomPreview",
|
"TimelineItemEventRowForDirectRoomPreview",
|
||||||
|
"TimelineItemEventRowShieldPreview",
|
||||||
"TimelineItemEventRowTimestampPreview",
|
"TimelineItemEventRowTimestampPreview",
|
||||||
"TimelineItemEventRowWithManyReactionsPreview",
|
"TimelineItemEventRowWithManyReactionsPreview",
|
||||||
"TimelineItemEventRowWithRRPreview",
|
"TimelineItemEventRowWithRRPreview",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue