Merge pull request #1910 from vector-im/feature/bma/fixRRscroll
Fix issues around read receipt
This commit is contained in:
commit
d0cc8c0b2f
269 changed files with 547 additions and 288 deletions
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline
|
package io.element.android.features.messages.impl.timeline
|
||||||
|
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData
|
||||||
import io.element.android.features.messages.impl.timeline.model.InReplyToDetails
|
import io.element.android.features.messages.impl.timeline.model.InReplyToDetails
|
||||||
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.ReadReceiptData
|
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
||||||
|
|
@ -187,17 +188,27 @@ internal fun aTimelineItemReadReceipts(): TimelineItemReadReceipts {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aGroupedEvents(id: Long = 0): TimelineItem.GroupedEvents {
|
internal fun aGroupedEvents(id: Long = 0): TimelineItem.GroupedEvents {
|
||||||
val event = aTimelineItemEvent(
|
val event1 = aTimelineItemEvent(
|
||||||
isMine = true,
|
isMine = true,
|
||||||
content = aTimelineItemStateEventContent(),
|
content = aTimelineItemStateEventContent(),
|
||||||
groupPosition = TimelineItemGroupPosition.None
|
groupPosition = TimelineItemGroupPosition.None,
|
||||||
|
readReceiptState = TimelineItemReadReceipts(
|
||||||
|
receipts = listOf(aReadReceiptData(0)).toPersistentList(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
val event2 = aTimelineItemEvent(
|
||||||
|
isMine = true,
|
||||||
|
content = aTimelineItemStateEventContent(body = "Another state event"),
|
||||||
|
groupPosition = TimelineItemGroupPosition.None,
|
||||||
|
readReceiptState = TimelineItemReadReceipts(
|
||||||
|
receipts = listOf(aReadReceiptData(1)).toPersistentList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val events = listOf(event1, event2)
|
||||||
return TimelineItem.GroupedEvents(
|
return TimelineItem.GroupedEvents(
|
||||||
id = id.toString(),
|
id = id.toString(),
|
||||||
events = listOf(
|
events = events.toImmutableList(),
|
||||||
event,
|
aggregatedReadReceipts = events.flatMap { it.readReceiptState.receipts }.toImmutableList(),
|
||||||
event,
|
|
||||||
).toImmutableList()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,11 @@ package io.element.android.features.messages.impl.timeline
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.animation.animateContentSize
|
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.scaleIn
|
import androidx.compose.animation.scaleIn
|
||||||
import androidx.compose.animation.scaleOut
|
import androidx.compose.animation.scaleOut
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
|
@ -42,26 +40,19 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.platform.LocalInspectionMode
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
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
|
||||||
import io.element.android.features.messages.impl.R
|
import io.element.android.features.messages.impl.timeline.components.TimelineItemRow
|
||||||
import io.element.android.features.messages.impl.timeline.components.TimelineItemEventRow
|
|
||||||
import io.element.android.features.messages.impl.timeline.components.TimelineItemStateEventRow
|
|
||||||
import io.element.android.features.messages.impl.timeline.components.TimelineItemVirtualRow
|
|
||||||
import io.element.android.features.messages.impl.timeline.components.group.GroupHeaderView
|
|
||||||
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemRoomBeginningView
|
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemRoomBeginningView
|
||||||
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator
|
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator
|
||||||
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
||||||
|
|
@ -70,9 +61,6 @@ 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.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo
|
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
|
||||||
import io.element.android.libraries.designsystem.animation.alphaAnimation
|
import io.element.android.libraries.designsystem.animation.alphaAnimation
|
||||||
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
|
||||||
|
|
@ -174,115 +162,6 @@ fun TimelineView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun TimelineItemRow(
|
|
||||||
timelineItem: TimelineItem,
|
|
||||||
showReadReceipts: Boolean,
|
|
||||||
isLastOutgoingMessage: Boolean,
|
|
||||||
highlightedItem: String?,
|
|
||||||
userHasPermissionToSendMessage: Boolean,
|
|
||||||
sessionState: SessionState,
|
|
||||||
onUserDataClick: (UserId) -> Unit,
|
|
||||||
onClick: (TimelineItem.Event) -> Unit,
|
|
||||||
onLongClick: (TimelineItem.Event) -> Unit,
|
|
||||||
inReplyToClick: (EventId) -> Unit,
|
|
||||||
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
|
||||||
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
|
||||||
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
|
||||||
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
|
||||||
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
|
||||||
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
|
||||||
eventSink: (TimelineEvents) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
when (timelineItem) {
|
|
||||||
is TimelineItem.Virtual -> {
|
|
||||||
TimelineItemVirtualRow(
|
|
||||||
virtual = timelineItem,
|
|
||||||
sessionState = sessionState,
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is TimelineItem.Event -> {
|
|
||||||
if (timelineItem.content is TimelineItemStateContent) {
|
|
||||||
TimelineItemStateEventRow(
|
|
||||||
event = timelineItem,
|
|
||||||
isHighlighted = highlightedItem == timelineItem.identifier(),
|
|
||||||
onClick = { onClick(timelineItem) },
|
|
||||||
onLongClick = { onLongClick(timelineItem) },
|
|
||||||
eventSink = eventSink,
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
TimelineItemEventRow(
|
|
||||||
event = timelineItem,
|
|
||||||
showReadReceipts = showReadReceipts,
|
|
||||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
|
||||||
isHighlighted = highlightedItem == timelineItem.identifier(),
|
|
||||||
canReply = userHasPermissionToSendMessage && timelineItem.content.canBeRepliedTo(),
|
|
||||||
onClick = { onClick(timelineItem) },
|
|
||||||
onLongClick = { onLongClick(timelineItem) },
|
|
||||||
onUserDataClick = onUserDataClick,
|
|
||||||
inReplyToClick = inReplyToClick,
|
|
||||||
onReactionClick = onReactionClick,
|
|
||||||
onReactionLongClick = onReactionLongClick,
|
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
|
||||||
onReadReceiptClick = onReadReceiptClick,
|
|
||||||
onTimestampClicked = onTimestampClicked,
|
|
||||||
onSwipeToReply = { onSwipeToReply(timelineItem) },
|
|
||||||
eventSink = eventSink,
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is TimelineItem.GroupedEvents -> {
|
|
||||||
val isExpanded = rememberSaveable(key = timelineItem.identifier()) { mutableStateOf(false) }
|
|
||||||
|
|
||||||
fun onExpandGroupClick() {
|
|
||||||
isExpanded.value = !isExpanded.value
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(modifier = modifier.animateContentSize()) {
|
|
||||||
GroupHeaderView(
|
|
||||||
text = pluralStringResource(
|
|
||||||
id = R.plurals.room_timeline_state_changes,
|
|
||||||
count = timelineItem.events.size,
|
|
||||||
timelineItem.events.size
|
|
||||||
),
|
|
||||||
isExpanded = isExpanded.value,
|
|
||||||
isHighlighted = !isExpanded.value && timelineItem.events.any { it.identifier() == highlightedItem },
|
|
||||||
onClick = ::onExpandGroupClick,
|
|
||||||
)
|
|
||||||
if (isExpanded.value) {
|
|
||||||
Column {
|
|
||||||
timelineItem.events.forEach { subGroupEvent ->
|
|
||||||
TimelineItemRow(
|
|
||||||
timelineItem = subGroupEvent,
|
|
||||||
showReadReceipts = showReadReceipts,
|
|
||||||
isLastOutgoingMessage = isLastOutgoingMessage,
|
|
||||||
highlightedItem = highlightedItem,
|
|
||||||
sessionState = sessionState,
|
|
||||||
userHasPermissionToSendMessage = false,
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick,
|
|
||||||
inReplyToClick = inReplyToClick,
|
|
||||||
onUserDataClick = onUserDataClick,
|
|
||||||
onTimestampClicked = onTimestampClicked,
|
|
||||||
onReactionClick = onReactionClick,
|
|
||||||
onReactionLongClick = onReactionLongClick,
|
|
||||||
onMoreReactionsClick = onMoreReactionsClick,
|
|
||||||
onReadReceiptClick = onReadReceiptClick,
|
|
||||||
eventSink = eventSink,
|
|
||||||
onSwipeToReply = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BoxScope.TimelineScrollHelper(
|
private fun BoxScope.TimelineScrollHelper(
|
||||||
isTimelineEmpty: Boolean,
|
isTimelineEmpty: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 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.animation.animateContentSize
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import io.element.android.features.messages.impl.R
|
||||||
|
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||||
|
import io.element.android.features.messages.impl.timeline.aGroupedEvents
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.group.GroupHeaderView
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.features.messages.impl.timeline.session.SessionState
|
||||||
|
import io.element.android.features.messages.impl.timeline.session.aSessionState
|
||||||
|
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
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TimelineItemGroupedEventsRow(
|
||||||
|
timelineItem: TimelineItem.GroupedEvents,
|
||||||
|
showReadReceipts: Boolean,
|
||||||
|
isLastOutgoingMessage: Boolean,
|
||||||
|
highlightedItem: String?,
|
||||||
|
sessionState: SessionState,
|
||||||
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
|
inReplyToClick: (EventId) -> Unit,
|
||||||
|
onUserDataClick: (UserId) -> Unit,
|
||||||
|
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||||
|
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
|
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
|
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
||||||
|
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||||
|
eventSink: (TimelineEvents) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val isExpanded = rememberSaveable(key = timelineItem.identifier()) { mutableStateOf(false) }
|
||||||
|
|
||||||
|
fun onExpandGroupClick() {
|
||||||
|
isExpanded.value = !isExpanded.value
|
||||||
|
}
|
||||||
|
|
||||||
|
TimelineItemGroupedEventsRowContent(
|
||||||
|
isExpanded = isExpanded.value,
|
||||||
|
onExpandGroupClick = ::onExpandGroupClick,
|
||||||
|
timelineItem = timelineItem,
|
||||||
|
highlightedItem = highlightedItem,
|
||||||
|
showReadReceipts = showReadReceipts,
|
||||||
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
|
sessionState = sessionState,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
inReplyToClick = inReplyToClick,
|
||||||
|
onUserDataClick = onUserDataClick,
|
||||||
|
onTimestampClicked = onTimestampClicked,
|
||||||
|
onReactionClick = onReactionClick,
|
||||||
|
onReactionLongClick = onReactionLongClick,
|
||||||
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
|
eventSink = eventSink,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TimelineItemGroupedEventsRowContent(
|
||||||
|
isExpanded: Boolean,
|
||||||
|
onExpandGroupClick: () -> Unit,
|
||||||
|
timelineItem: TimelineItem.GroupedEvents,
|
||||||
|
highlightedItem: String?,
|
||||||
|
showReadReceipts: Boolean,
|
||||||
|
isLastOutgoingMessage: Boolean,
|
||||||
|
sessionState: SessionState,
|
||||||
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
|
inReplyToClick: (EventId) -> Unit,
|
||||||
|
onUserDataClick: (UserId) -> Unit,
|
||||||
|
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||||
|
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
|
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
|
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
||||||
|
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||||
|
eventSink: (TimelineEvents) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier.animateContentSize()) {
|
||||||
|
GroupHeaderView(
|
||||||
|
text = pluralStringResource(
|
||||||
|
id = R.plurals.room_timeline_state_changes,
|
||||||
|
count = timelineItem.events.size,
|
||||||
|
timelineItem.events.size
|
||||||
|
),
|
||||||
|
isExpanded = isExpanded,
|
||||||
|
isHighlighted = !isExpanded && timelineItem.events.any { it.identifier() == highlightedItem },
|
||||||
|
onClick = onExpandGroupClick,
|
||||||
|
)
|
||||||
|
if (isExpanded) {
|
||||||
|
Column {
|
||||||
|
timelineItem.events.forEach { subGroupEvent ->
|
||||||
|
TimelineItemRow(
|
||||||
|
timelineItem = subGroupEvent,
|
||||||
|
showReadReceipts = showReadReceipts,
|
||||||
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
|
highlightedItem = highlightedItem,
|
||||||
|
sessionState = sessionState,
|
||||||
|
userHasPermissionToSendMessage = false,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
inReplyToClick = inReplyToClick,
|
||||||
|
onUserDataClick = onUserDataClick,
|
||||||
|
onTimestampClicked = onTimestampClicked,
|
||||||
|
onReactionClick = onReactionClick,
|
||||||
|
onReactionLongClick = onReactionLongClick,
|
||||||
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
|
eventSink = eventSink,
|
||||||
|
onSwipeToReply = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (showReadReceipts) {
|
||||||
|
TimelineItemReadReceiptView(
|
||||||
|
state = ReadReceiptViewState(
|
||||||
|
sendState = null,
|
||||||
|
isLastOutgoingMessage = false,
|
||||||
|
receipts = timelineItem.aggregatedReadReceipts,
|
||||||
|
),
|
||||||
|
showReadReceipts = true,
|
||||||
|
onReadReceiptsClicked = { /* No op for group event */ })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemGroupedEventsRowContentExpandedPreview() = ElementPreview {
|
||||||
|
TimelineItemGroupedEventsRowContent(
|
||||||
|
isExpanded = true,
|
||||||
|
onExpandGroupClick = {},
|
||||||
|
timelineItem = aGroupedEvents(),
|
||||||
|
highlightedItem = null,
|
||||||
|
showReadReceipts = true,
|
||||||
|
isLastOutgoingMessage = false,
|
||||||
|
sessionState = aSessionState(),
|
||||||
|
onClick = {},
|
||||||
|
onLongClick = {},
|
||||||
|
inReplyToClick = {},
|
||||||
|
onUserDataClick = {},
|
||||||
|
onTimestampClicked = {},
|
||||||
|
onReactionClick = { _, _ -> },
|
||||||
|
onReactionLongClick = { _, _ -> },
|
||||||
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
|
eventSink = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemGroupedEventsRowContentCollapsePreview() = ElementPreview {
|
||||||
|
TimelineItemGroupedEventsRowContent(
|
||||||
|
isExpanded = false,
|
||||||
|
onExpandGroupClick = {},
|
||||||
|
timelineItem = aGroupedEvents(),
|
||||||
|
highlightedItem = null,
|
||||||
|
showReadReceipts = true,
|
||||||
|
isLastOutgoingMessage = false,
|
||||||
|
sessionState = aSessionState(),
|
||||||
|
onClick = {},
|
||||||
|
onLongClick = {},
|
||||||
|
inReplyToClick = {},
|
||||||
|
onUserDataClick = {},
|
||||||
|
onTimestampClicked = {},
|
||||||
|
onReactionClick = { _, _ -> },
|
||||||
|
onReactionLongClick = { _, _ -> },
|
||||||
|
onMoreReactionsClick = {},
|
||||||
|
onReadReceiptClick = {},
|
||||||
|
eventSink = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 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.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.event.canBeRepliedTo
|
||||||
|
import io.element.android.features.messages.impl.timeline.session.SessionState
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun TimelineItemRow(
|
||||||
|
timelineItem: TimelineItem,
|
||||||
|
showReadReceipts: Boolean,
|
||||||
|
isLastOutgoingMessage: Boolean,
|
||||||
|
highlightedItem: String?,
|
||||||
|
userHasPermissionToSendMessage: Boolean,
|
||||||
|
sessionState: SessionState,
|
||||||
|
onUserDataClick: (UserId) -> Unit,
|
||||||
|
onClick: (TimelineItem.Event) -> Unit,
|
||||||
|
onLongClick: (TimelineItem.Event) -> Unit,
|
||||||
|
inReplyToClick: (EventId) -> Unit,
|
||||||
|
onReactionClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
|
onReactionLongClick: (key: String, TimelineItem.Event) -> Unit,
|
||||||
|
onMoreReactionsClick: (TimelineItem.Event) -> Unit,
|
||||||
|
onReadReceiptClick: (TimelineItem.Event) -> Unit,
|
||||||
|
onTimestampClicked: (TimelineItem.Event) -> Unit,
|
||||||
|
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
||||||
|
eventSink: (TimelineEvents) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
when (timelineItem) {
|
||||||
|
is TimelineItem.Virtual -> {
|
||||||
|
TimelineItemVirtualRow(
|
||||||
|
virtual = timelineItem,
|
||||||
|
sessionState = sessionState,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is TimelineItem.Event -> {
|
||||||
|
if (timelineItem.content is TimelineItemStateContent) {
|
||||||
|
TimelineItemStateEventRow(
|
||||||
|
event = timelineItem,
|
||||||
|
showReadReceipts = showReadReceipts,
|
||||||
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
|
isHighlighted = highlightedItem == timelineItem.identifier(),
|
||||||
|
onClick = { onClick(timelineItem) },
|
||||||
|
onReadReceiptsClick = onReadReceiptClick,
|
||||||
|
onLongClick = { onLongClick(timelineItem) },
|
||||||
|
eventSink = eventSink,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
TimelineItemEventRow(
|
||||||
|
event = timelineItem,
|
||||||
|
showReadReceipts = showReadReceipts,
|
||||||
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
|
isHighlighted = highlightedItem == timelineItem.identifier(),
|
||||||
|
canReply = userHasPermissionToSendMessage && timelineItem.content.canBeRepliedTo(),
|
||||||
|
onClick = { onClick(timelineItem) },
|
||||||
|
onLongClick = { onLongClick(timelineItem) },
|
||||||
|
onUserDataClick = onUserDataClick,
|
||||||
|
inReplyToClick = inReplyToClick,
|
||||||
|
onReactionClick = onReactionClick,
|
||||||
|
onReactionLongClick = onReactionLongClick,
|
||||||
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
|
onTimestampClicked = onTimestampClicked,
|
||||||
|
onSwipeToReply = { onSwipeToReply(timelineItem) },
|
||||||
|
eventSink = eventSink,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TimelineItem.GroupedEvents -> {
|
||||||
|
TimelineItemGroupedEventsRow(
|
||||||
|
timelineItem = timelineItem,
|
||||||
|
showReadReceipts = showReadReceipts,
|
||||||
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
|
highlightedItem = highlightedItem,
|
||||||
|
sessionState = sessionState,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
inReplyToClick = inReplyToClick,
|
||||||
|
onUserDataClick = onUserDataClick,
|
||||||
|
onTimestampClicked = onTimestampClicked,
|
||||||
|
onReactionClick = onReactionClick,
|
||||||
|
onReactionLongClick = onReactionLongClick,
|
||||||
|
onMoreReactionsClick = onMoreReactionsClick,
|
||||||
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
|
eventSink = eventSink,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.components
|
||||||
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
|
@ -32,51 +33,73 @@ import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
import io.element.android.features.messages.impl.timeline.aTimelineItemEvent
|
||||||
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView
|
||||||
import io.element.android.features.messages.impl.timeline.components.event.noExtraPadding
|
import io.element.android.features.messages.impl.timeline.components.event.noExtraPadding
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.receipt.aReadReceiptData
|
||||||
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.TimelineItemGroupPosition
|
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.util.defaultTimelineContentPadding
|
import io.element.android.features.messages.impl.timeline.util.defaultTimelineContentPadding
|
||||||
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 kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemStateEventRow(
|
fun TimelineItemStateEventRow(
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
|
showReadReceipts: Boolean,
|
||||||
|
isLastOutgoingMessage: Boolean,
|
||||||
isHighlighted: Boolean,
|
isHighlighted: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
|
onReadReceiptsClick: (event: TimelineItem.Event) -> Unit,
|
||||||
eventSink: (TimelineEvents) -> Unit,
|
eventSink: (TimelineEvents) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
Box(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
.wrapContentHeight(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
) {
|
||||||
MessageStateEventContainer(
|
Box(
|
||||||
isHighlighted = isHighlighted,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.zIndex(-1f)
|
.fillMaxWidth()
|
||||||
.widthIn(max = 320.dp)
|
.padding(top = 8.dp, bottom = 2.dp)
|
||||||
|
.wrapContentHeight(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
TimelineItemEventContentView(
|
MessageStateEventContainer(
|
||||||
content = event.content,
|
isHighlighted = isHighlighted,
|
||||||
isMine = event.isMine,
|
|
||||||
isEditable = event.isEditable,
|
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
extraPadding = noExtraPadding,
|
modifier = Modifier
|
||||||
eventSink = eventSink,
|
.zIndex(-1f)
|
||||||
modifier = Modifier.defaultTimelineContentPadding()
|
.widthIn(max = 320.dp)
|
||||||
)
|
) {
|
||||||
|
TimelineItemEventContentView(
|
||||||
|
content = event.content,
|
||||||
|
isMine = event.isMine,
|
||||||
|
isEditable = event.isEditable,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
extraPadding = noExtraPadding,
|
||||||
|
eventSink = eventSink,
|
||||||
|
modifier = Modifier.defaultTimelineContentPadding()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
TimelineItemReadReceiptView(
|
||||||
|
state = ReadReceiptViewState(
|
||||||
|
sendState = event.localSendState,
|
||||||
|
isLastOutgoingMessage = isLastOutgoingMessage,
|
||||||
|
receipts = event.readReceiptState.receipts,
|
||||||
|
),
|
||||||
|
showReadReceipts = showReadReceipts,
|
||||||
|
onReadReceiptsClicked = { onReadReceiptsClick(event) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,11 +110,17 @@ internal fun TimelineItemStateEventRowPreview() = ElementPreview {
|
||||||
event = aTimelineItemEvent(
|
event = aTimelineItemEvent(
|
||||||
isMine = false,
|
isMine = false,
|
||||||
content = aTimelineItemStateEventContent(),
|
content = aTimelineItemStateEventContent(),
|
||||||
groupPosition = TimelineItemGroupPosition.None
|
groupPosition = TimelineItemGroupPosition.None,
|
||||||
|
readReceiptState = TimelineItemReadReceipts(
|
||||||
|
receipts = listOf(aReadReceiptData(0)).toPersistentList(),
|
||||||
|
)
|
||||||
),
|
),
|
||||||
|
showReadReceipts = true,
|
||||||
|
isLastOutgoingMessage = false,
|
||||||
isHighlighted = false,
|
isHighlighted = false,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
|
onReadReceiptsClick = {},
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,23 +33,23 @@ class ReadReceiptViewStateProvider : PreviewParameterProvider<ReadReceiptViewSta
|
||||||
aReadReceiptViewState(sendState = LocalEventSendState.Sent(EventId("\$eventId"))),
|
aReadReceiptViewState(sendState = LocalEventSendState.Sent(EventId("\$eventId"))),
|
||||||
aReadReceiptViewState(
|
aReadReceiptViewState(
|
||||||
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||||
receipts = mutableListOf<ReadReceiptData>().apply { repeat(1) { add(aReadReceiptData(it)) } },
|
receipts = List(1) { aReadReceiptData(it) },
|
||||||
),
|
),
|
||||||
aReadReceiptViewState(
|
aReadReceiptViewState(
|
||||||
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||||
receipts = mutableListOf<ReadReceiptData>().apply { repeat(2) { add(aReadReceiptData(it)) } },
|
receipts = List(2) { aReadReceiptData(it) },
|
||||||
),
|
),
|
||||||
aReadReceiptViewState(
|
aReadReceiptViewState(
|
||||||
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||||
receipts = mutableListOf<ReadReceiptData>().apply { repeat(3) { add(aReadReceiptData(it)) } },
|
receipts = List(3) { aReadReceiptData(it) },
|
||||||
),
|
),
|
||||||
aReadReceiptViewState(
|
aReadReceiptViewState(
|
||||||
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||||
receipts = mutableListOf<ReadReceiptData>().apply { repeat(4) { add(aReadReceiptData(it)) } },
|
receipts = List(4) { aReadReceiptData(it) },
|
||||||
),
|
),
|
||||||
aReadReceiptViewState(
|
aReadReceiptViewState(
|
||||||
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||||
receipts = mutableListOf<ReadReceiptData>().apply { repeat(5) { add(aReadReceiptData(it)) } },
|
receipts = List(5) { aReadReceiptData(it) },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,10 @@ package io.element.android.features.messages.impl.timeline.components.receipt.bo
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -29,6 +30,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
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
|
||||||
|
|
@ -38,7 +40,6 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
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.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||||
import io.element.android.compound.theme.ElementTheme
|
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
@ -83,34 +84,39 @@ internal fun ReadReceiptBottomSheet(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ColumnScope.ReadReceiptBottomSheetContent(
|
private fun ReadReceiptBottomSheetContent(
|
||||||
state: ReadReceiptBottomSheetState,
|
state: ReadReceiptBottomSheetState,
|
||||||
onUserDataClicked: (UserId) -> Unit,
|
onUserDataClicked: (UserId) -> Unit,
|
||||||
) {
|
) {
|
||||||
ListItem(
|
LazyColumn {
|
||||||
headlineContent = {
|
item {
|
||||||
Text(text = stringResource(id = CommonStrings.common_seen_by))
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(text = stringResource(id = CommonStrings.common_seen_by))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(
|
||||||
|
items = state.selectedEvent?.readReceiptState?.receipts.orEmpty()
|
||||||
|
) {
|
||||||
|
val userId = UserId(it.avatarData.id)
|
||||||
|
MatrixUserRow(
|
||||||
|
modifier = Modifier.clickable { onUserDataClicked(userId) },
|
||||||
|
matrixUser = MatrixUser(
|
||||||
|
userId = userId,
|
||||||
|
displayName = it.avatarData.name,
|
||||||
|
avatarUrl = it.avatarData.url,
|
||||||
|
),
|
||||||
|
avatarSize = AvatarSize.ReadReceiptList,
|
||||||
|
trailingContent = {
|
||||||
|
Text(
|
||||||
|
text = it.formattedDate,
|
||||||
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
val receipts = state.selectedEvent?.readReceiptState?.receipts.orEmpty()
|
|
||||||
receipts.forEach {
|
|
||||||
val userId = UserId(it.avatarData.id)
|
|
||||||
MatrixUserRow(
|
|
||||||
modifier = Modifier.clickable { onUserDataClicked(userId) },
|
|
||||||
matrixUser = MatrixUser(
|
|
||||||
userId = userId,
|
|
||||||
displayName = it.avatarData.name,
|
|
||||||
avatarUrl = it.avatarData.url,
|
|
||||||
),
|
|
||||||
avatarSize = AvatarSize.ReadReceiptList,
|
|
||||||
trailingContent = {
|
|
||||||
Text(
|
|
||||||
text = it.formattedDate,
|
|
||||||
style = ElementTheme.typography.fontBodySmRegular,
|
|
||||||
color = ElementTheme.colors.textSecondary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,8 @@ private fun MutableList<TimelineItem>.addGroup(
|
||||||
add(
|
add(
|
||||||
TimelineItem.GroupedEvents(
|
TimelineItem.GroupedEvents(
|
||||||
id = groupId,
|
id = groupId,
|
||||||
events = groupOfItems.toImmutableList()
|
events = groupOfItems.toImmutableList(),
|
||||||
|
aggregatedReadReceipts = groupOfItems.flatMap { it.readReceiptState.receipts }.toImmutableList()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,6 @@ sealed interface TimelineItem {
|
||||||
data class GroupedEvents(
|
data class GroupedEvents(
|
||||||
val id: String,
|
val id: String,
|
||||||
val events: ImmutableList<Event>,
|
val events: ImmutableList<Event>,
|
||||||
|
val aggregatedReadReceipts: ImmutableList<ReadReceiptData>,
|
||||||
) : TimelineItem
|
) : TimelineItem
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,8 @@ fun aTimelineItemTextContent() = TimelineItemTextContent(
|
||||||
|
|
||||||
fun aTimelineItemUnknownContent() = TimelineItemUnknownContent
|
fun aTimelineItemUnknownContent() = TimelineItemUnknownContent
|
||||||
|
|
||||||
fun aTimelineItemStateEventContent() = TimelineItemStateEventContent(
|
fun aTimelineItemStateEventContent(
|
||||||
body = "A state event",
|
body: String = "A state event",
|
||||||
|
) = TimelineItemStateEventContent(
|
||||||
|
body = body,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -86,11 +86,12 @@ class TimelineItemGrouperTest {
|
||||||
assertThat(result).isEqualTo(
|
assertThat(result).isEqualTo(
|
||||||
listOf(
|
listOf(
|
||||||
TimelineItem.GroupedEvents(
|
TimelineItem.GroupedEvents(
|
||||||
computeGroupIdWith(aGroupableItem),
|
id = computeGroupIdWith(aGroupableItem),
|
||||||
events = listOf(
|
events = listOf(
|
||||||
aGroupableItem.copy("0"),
|
aGroupableItem.copy("0"),
|
||||||
aGroupableItem.copy(id = "1"),
|
aGroupableItem.copy(id = "1"),
|
||||||
).toImmutableList()
|
).toImmutableList(),
|
||||||
|
aggregatedReadReceipts = emptyList<ReadReceiptData>().toImmutableList(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -132,20 +133,22 @@ class TimelineItemGrouperTest {
|
||||||
assertThat(result).isEqualTo(
|
assertThat(result).isEqualTo(
|
||||||
listOf(
|
listOf(
|
||||||
TimelineItem.GroupedEvents(
|
TimelineItem.GroupedEvents(
|
||||||
computeGroupIdWith(aGroupableItem),
|
id = computeGroupIdWith(aGroupableItem),
|
||||||
events = listOf(
|
events = listOf(
|
||||||
aGroupableItem,
|
aGroupableItem,
|
||||||
aGroupableItem,
|
aGroupableItem,
|
||||||
).toImmutableList()
|
).toImmutableList(),
|
||||||
|
aggregatedReadReceipts = emptyList<ReadReceiptData>().toImmutableList(),
|
||||||
),
|
),
|
||||||
aNonGroupableItem,
|
aNonGroupableItem,
|
||||||
TimelineItem.GroupedEvents(
|
TimelineItem.GroupedEvents(
|
||||||
computeGroupIdWith(aGroupableItem),
|
id = computeGroupIdWith(aGroupableItem),
|
||||||
events = listOf(
|
events = listOf(
|
||||||
aGroupableItem,
|
aGroupableItem,
|
||||||
aGroupableItem,
|
aGroupableItem,
|
||||||
aGroupableItem,
|
aGroupableItem,
|
||||||
).toImmutableList()
|
).toImmutableList(),
|
||||||
|
aggregatedReadReceipts = emptyList<ReadReceiptData>().toImmutableList(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue