Timeline permalink : add focused event modifier

This commit is contained in:
ganfra 2024-04-23 16:58:23 +02:00
parent 64d02d17bb
commit 1c06db1a78
5 changed files with 104 additions and 60 deletions

View file

@ -146,7 +146,7 @@ fun TimelineView(
renderReadReceipts = state.renderReadReceipts, renderReadReceipts = state.renderReadReceipts,
isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true && isLastOutgoingMessage = (timelineItem as? TimelineItem.Event)?.isMine == true &&
state.timelineItems.first().identifier() == timelineItem.identifier(), state.timelineItems.first().identifier() == timelineItem.identifier(),
highlightedItem = state.focusedEventId?.value, focusedEventId = state.focusedEventId,
onClick = onMessageClicked, onClick = onMessageClicked,
onLongClick = onMessageLongClicked, onLongClick = onMessageLongClicked,
onUserDataClick = onUserDataClicked, onUserDataClick = onUserDataClicked,

View file

@ -43,7 +43,7 @@ fun TimelineItemGroupedEventsRow(
timelineRoomInfo: TimelineRoomInfo, timelineRoomInfo: TimelineRoomInfo,
renderReadReceipts: Boolean, renderReadReceipts: Boolean,
isLastOutgoingMessage: Boolean, isLastOutgoingMessage: Boolean,
highlightedItem: String?, focusedEventId: EventId?,
onClick: (TimelineItem.Event) -> Unit, onClick: (TimelineItem.Event) -> Unit,
onLongClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit,
inReplyToClick: (EventId) -> Unit, inReplyToClick: (EventId) -> Unit,
@ -68,7 +68,7 @@ fun TimelineItemGroupedEventsRow(
onExpandGroupClick = ::onExpandGroupClick, onExpandGroupClick = ::onExpandGroupClick,
timelineItem = timelineItem, timelineItem = timelineItem,
timelineRoomInfo = timelineRoomInfo, timelineRoomInfo = timelineRoomInfo,
highlightedItem = highlightedItem, focusedEventId = focusedEventId,
renderReadReceipts = renderReadReceipts, renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage, isLastOutgoingMessage = isLastOutgoingMessage,
onClick = onClick, onClick = onClick,
@ -92,7 +92,7 @@ private fun TimelineItemGroupedEventsRowContent(
onExpandGroupClick: () -> Unit, onExpandGroupClick: () -> Unit,
timelineItem: TimelineItem.GroupedEvents, timelineItem: TimelineItem.GroupedEvents,
timelineRoomInfo: TimelineRoomInfo, timelineRoomInfo: TimelineRoomInfo,
highlightedItem: String?, focusedEventId: EventId?,
renderReadReceipts: Boolean, renderReadReceipts: Boolean,
isLastOutgoingMessage: Boolean, isLastOutgoingMessage: Boolean,
onClick: (TimelineItem.Event) -> Unit, onClick: (TimelineItem.Event) -> Unit,
@ -116,7 +116,7 @@ private fun TimelineItemGroupedEventsRowContent(
timelineItem.events.size timelineItem.events.size
), ),
isExpanded = isExpanded, isExpanded = isExpanded,
isHighlighted = !isExpanded && timelineItem.events.any { it.identifier() == highlightedItem }, isHighlighted = !isExpanded && timelineItem.events.any { it.isEvent(focusedEventId) },
onClick = onExpandGroupClick, onClick = onExpandGroupClick,
) )
if (isExpanded) { if (isExpanded) {
@ -127,7 +127,7 @@ private fun TimelineItemGroupedEventsRowContent(
timelineRoomInfo = timelineRoomInfo, timelineRoomInfo = timelineRoomInfo,
renderReadReceipts = renderReadReceipts, renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage, isLastOutgoingMessage = isLastOutgoingMessage,
highlightedItem = highlightedItem, focusedEventId = focusedEventId,
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
inReplyToClick = inReplyToClick, inReplyToClick = inReplyToClick,
@ -165,7 +165,7 @@ internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPrevi
onExpandGroupClick = {}, onExpandGroupClick = {},
timelineItem = aGroupedEvents(withReadReceipts = true), timelineItem = aGroupedEvents(withReadReceipts = true),
timelineRoomInfo = aTimelineRoomInfo(), timelineRoomInfo = aTimelineRoomInfo(),
highlightedItem = null, focusedEventId = null,
renderReadReceipts = true, renderReadReceipts = true,
isLastOutgoingMessage = false, isLastOutgoingMessage = false,
onClick = {}, onClick = {},
@ -190,7 +190,7 @@ internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPrevi
onExpandGroupClick = {}, onExpandGroupClick = {},
timelineItem = aGroupedEvents(withReadReceipts = true), timelineItem = aGroupedEvents(withReadReceipts = true),
timelineRoomInfo = aTimelineRoomInfo(), timelineRoomInfo = aTimelineRoomInfo(),
highlightedItem = null, focusedEventId = null,
renderReadReceipts = true, renderReadReceipts = true,
isLastOutgoingMessage = false, isLastOutgoingMessage = false,
onClick = {}, onClick = {},

View file

@ -16,13 +16,23 @@
package io.element.android.features.messages.impl.timeline.components package io.element.android.features.messages.impl.timeline.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
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.TimelineItemLegacyCallInviteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
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.EventId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@ -32,7 +42,7 @@ internal fun TimelineItemRow(
timelineRoomInfo: TimelineRoomInfo, timelineRoomInfo: TimelineRoomInfo,
renderReadReceipts: Boolean, renderReadReceipts: Boolean,
isLastOutgoingMessage: Boolean, isLastOutgoingMessage: Boolean,
highlightedItem: String?, focusedEventId: EventId?,
onUserDataClick: (UserId) -> Unit, onUserDataClick: (UserId) -> Unit,
onLinkClicked: (String) -> Unit, onLinkClicked: (String) -> Unit,
onClick: (TimelineItem.Event) -> Unit, onClick: (TimelineItem.Event) -> Unit,
@ -47,71 +57,92 @@ internal fun TimelineItemRow(
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit, eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
when (timelineItem) {
is TimelineItem.Virtual -> { val backgroundModifier = if (timelineItem.isEvent(focusedEventId)) {
TimelineItemVirtualRow( Modifier.focusedEvent()
virtual = timelineItem, } else {
timelineRoomInfo = timelineRoomInfo, Modifier
eventSink = eventSink, }
modifier = modifier, Box(modifier = modifier.then(backgroundModifier)) {
) when (timelineItem) {
} is TimelineItem.Virtual -> {
is TimelineItem.Event -> { TimelineItemVirtualRow(
if (timelineItem.content is TimelineItemStateContent || timelineItem.content is TimelineItemLegacyCallInviteContent) { virtual = timelineItem,
TimelineItemStateEventRow( timelineRoomInfo = timelineRoomInfo,
event = timelineItem,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = highlightedItem == timelineItem.identifier(),
onClick = { onClick(timelineItem) },
onReadReceiptsClick = onReadReceiptClick,
onLongClick = { onLongClick(timelineItem) },
eventSink = eventSink, eventSink = eventSink,
modifier = modifier,
) )
} else { }
TimelineItemEventRow( is TimelineItem.Event -> {
event = timelineItem, if (timelineItem.content is TimelineItemStateContent || timelineItem.content is TimelineItemLegacyCallInviteContent) {
TimelineItemStateEventRow(
event = timelineItem,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = timelineItem.isEvent(focusedEventId),
onClick = { onClick(timelineItem) },
onReadReceiptsClick = onReadReceiptClick,
onLongClick = { onLongClick(timelineItem) },
eventSink = eventSink,
)
} else {
TimelineItemEventRow(
event = timelineItem,
timelineRoomInfo = timelineRoomInfo,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = timelineItem.isEvent(focusedEventId),
onClick = { onClick(timelineItem) },
onLongClick = { onLongClick(timelineItem) },
onUserDataClick = onUserDataClick,
onLinkClicked = onLinkClicked,
inReplyToClick = inReplyToClick,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,
onReadReceiptClick = onReadReceiptClick,
onTimestampClicked = onTimestampClicked,
onSwipeToReply = { onSwipeToReply(timelineItem) },
eventSink = eventSink,
)
}
}
is TimelineItem.GroupedEvents -> {
TimelineItemGroupedEventsRow(
timelineItem = timelineItem,
timelineRoomInfo = timelineRoomInfo, timelineRoomInfo = timelineRoomInfo,
renderReadReceipts = renderReadReceipts, renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage, isLastOutgoingMessage = isLastOutgoingMessage,
isHighlighted = highlightedItem == timelineItem.identifier(), focusedEventId = focusedEventId,
onClick = { onClick(timelineItem) }, onClick = onClick,
onLongClick = { onLongClick(timelineItem) }, onLongClick = onLongClick,
inReplyToClick = inReplyToClick,
onUserDataClick = onUserDataClick, onUserDataClick = onUserDataClick,
onLinkClicked = onLinkClicked, onLinkClicked = onLinkClicked,
inReplyToClick = inReplyToClick, onTimestampClicked = onTimestampClicked,
onReactionClick = onReactionClick, onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick, onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick, onMoreReactionsClick = onMoreReactionsClick,
onReadReceiptClick = onReadReceiptClick, onReadReceiptClick = onReadReceiptClick,
onTimestampClicked = onTimestampClicked,
onSwipeToReply = { onSwipeToReply(timelineItem) },
eventSink = eventSink, eventSink = eventSink,
modifier = modifier,
) )
} }
} }
is TimelineItem.GroupedEvents -> {
TimelineItemGroupedEventsRow(
timelineItem = timelineItem,
timelineRoomInfo = timelineRoomInfo,
renderReadReceipts = renderReadReceipts,
isLastOutgoingMessage = isLastOutgoingMessage,
highlightedItem = highlightedItem,
onClick = onClick,
onLongClick = onLongClick,
inReplyToClick = inReplyToClick,
onUserDataClick = onUserDataClick,
onLinkClicked = onLinkClicked,
onTimestampClicked = onTimestampClicked,
onReactionClick = onReactionClick,
onReactionLongClick = onReactionLongClick,
onMoreReactionsClick = onMoreReactionsClick,
onReadReceiptClick = onReadReceiptClick,
eventSink = eventSink,
modifier = modifier,
)
}
} }
} }
@Composable
private fun Modifier.focusedEvent(): Modifier {
val highlightedLineColor = ElementTheme.colors.textActionAccent
val gradientColors = listOf(
ElementTheme.colors.highlightedMessageBackgroundColor,
ElementTheme.materialColors.background
)
val verticalOffset = 2.dp.toPx()
return drawWithCache {
val brush = Brush.verticalGradient(gradientColors)
onDrawBehind {
drawRect(brush, topLeft = Offset(0f, verticalOffset), size = Size(size.width, size.height * 0.7f))
drawLine(highlightedLineColor, start = Offset(0f, verticalOffset), end = Offset(size.width, verticalOffset))
}
}.padding(top = 4.dp)
}

View file

@ -38,6 +38,14 @@ sealed interface TimelineItem {
is GroupedEvents -> id is GroupedEvents -> id
} }
fun isEvent(eventId: EventId?): Boolean {
if (eventId == null) return false
return when (this) {
is Event -> this.eventId == eventId
else -> false
}
}
fun contentType(): String = when (this) { fun contentType(): String = when (this) {
is Event -> content.type is Event -> content.type
is Virtual -> model.type is Virtual -> model.type

View file

@ -149,6 +149,11 @@ val SemanticColors.bigIconDefaultBackgroundColor
val SemanticColors.bigCheckmarkBorderColor val SemanticColors.bigCheckmarkBorderColor
get() = if (isLight) LightColorTokens.colorGray400 else DarkColorTokens.colorGray400 get() = if (isLight) LightColorTokens.colorGray400 else DarkColorTokens.colorGray400
@OptIn(CoreColorToken::class)
val SemanticColors.highlightedMessageBackgroundColor
get() = if (isLight) LightColorTokens.colorGreen300 else DarkColorTokens.colorGreen300
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun ColorAliasesPreview() = ElementPreview { internal fun ColorAliasesPreview() = ElementPreview {