Merge pull request #1910 from vector-im/feature/bma/fixRRscroll
Fix issues around read receipt
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
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.NewEventState
|
||||
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 {
|
||||
val event = aTimelineItemEvent(
|
||||
internal fun aGroupedEvents(id: Long = 0): TimelineItem.GroupedEvents {
|
||||
val event1 = aTimelineItemEvent(
|
||||
isMine = true,
|
||||
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(
|
||||
id = id.toString(),
|
||||
events = listOf(
|
||||
event,
|
||||
event,
|
||||
).toImmutableList()
|
||||
events = events.toImmutableList(),
|
||||
aggregatedReadReceipts = events.flatMap { it.readReceiptState.receipts }.toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,13 +20,11 @@ package io.element.android.features.messages.impl.timeline
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -42,26 +40,19 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.messages.impl.R
|
||||
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.TimelineItemRow
|
||||
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.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.event.TimelineItemEventContent
|
||||
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.preview.ElementPreview
|
||||
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
|
||||
private fun BoxScope.TimelineScrollHelper(
|
||||
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.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.components.event.TimelineItemEventContentView
|
||||
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.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.util.defaultTimelineContentPadding
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@Composable
|
||||
fun TimelineItemStateEventRow(
|
||||
event: TimelineItem.Event,
|
||||
showReadReceipts: Boolean,
|
||||
isLastOutgoingMessage: Boolean,
|
||||
isHighlighted: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
onReadReceiptsClick: (event: TimelineItem.Event) -> Unit,
|
||||
eventSink: (TimelineEvents) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Box(
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
.wrapContentHeight(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
MessageStateEventContainer(
|
||||
isHighlighted = isHighlighted,
|
||||
interactionSource = interactionSource,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.zIndex(-1f)
|
||||
.widthIn(max = 320.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp, bottom = 2.dp)
|
||||
.wrapContentHeight(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
TimelineItemEventContentView(
|
||||
content = event.content,
|
||||
isMine = event.isMine,
|
||||
isEditable = event.isEditable,
|
||||
MessageStateEventContainer(
|
||||
isHighlighted = isHighlighted,
|
||||
interactionSource = interactionSource,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
extraPadding = noExtraPadding,
|
||||
eventSink = eventSink,
|
||||
modifier = Modifier.defaultTimelineContentPadding()
|
||||
)
|
||||
modifier = Modifier
|
||||
.zIndex(-1f)
|
||||
.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(
|
||||
isMine = false,
|
||||
content = aTimelineItemStateEventContent(),
|
||||
groupPosition = TimelineItemGroupPosition.None
|
||||
groupPosition = TimelineItemGroupPosition.None,
|
||||
readReceiptState = TimelineItemReadReceipts(
|
||||
receipts = listOf(aReadReceiptData(0)).toPersistentList(),
|
||||
)
|
||||
),
|
||||
showReadReceipts = true,
|
||||
isLastOutgoingMessage = false,
|
||||
isHighlighted = false,
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
onReadReceiptsClick = {},
|
||||
eventSink = {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,23 +33,23 @@ class ReadReceiptViewStateProvider : PreviewParameterProvider<ReadReceiptViewSta
|
||||
aReadReceiptViewState(sendState = LocalEventSendState.Sent(EventId("\$eventId"))),
|
||||
aReadReceiptViewState(
|
||||
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||
receipts = mutableListOf<ReadReceiptData>().apply { repeat(1) { add(aReadReceiptData(it)) } },
|
||||
receipts = List(1) { aReadReceiptData(it) },
|
||||
),
|
||||
aReadReceiptViewState(
|
||||
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||
receipts = mutableListOf<ReadReceiptData>().apply { repeat(2) { add(aReadReceiptData(it)) } },
|
||||
receipts = List(2) { aReadReceiptData(it) },
|
||||
),
|
||||
aReadReceiptViewState(
|
||||
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||
receipts = mutableListOf<ReadReceiptData>().apply { repeat(3) { add(aReadReceiptData(it)) } },
|
||||
receipts = List(3) { aReadReceiptData(it) },
|
||||
),
|
||||
aReadReceiptViewState(
|
||||
sendState = LocalEventSendState.Sent(EventId("\$eventId")),
|
||||
receipts = mutableListOf<ReadReceiptData>().apply { repeat(4) { add(aReadReceiptData(it)) } },
|
||||
receipts = List(4) { aReadReceiptData(it) },
|
||||
),
|
||||
aReadReceiptViewState(
|
||||
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.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -29,6 +30,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
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.preview.ElementPreview
|
||||
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.user.MatrixUser
|
||||
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 kotlinx.coroutines.launch
|
||||
|
||||
@@ -83,34 +84,39 @@ internal fun ReadReceiptBottomSheet(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.ReadReceiptBottomSheetContent(
|
||||
private fun ReadReceiptBottomSheetContent(
|
||||
state: ReadReceiptBottomSheetState,
|
||||
onUserDataClicked: (UserId) -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(text = stringResource(id = CommonStrings.common_seen_by))
|
||||
LazyColumn {
|
||||
item {
|
||||
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(
|
||||
TimelineItem.GroupedEvents(
|
||||
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(
|
||||
val id: String,
|
||||
val events: ImmutableList<Event>,
|
||||
val aggregatedReadReceipts: ImmutableList<ReadReceiptData>,
|
||||
) : TimelineItem
|
||||
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@ fun aTimelineItemTextContent() = TimelineItemTextContent(
|
||||
|
||||
fun aTimelineItemUnknownContent() = TimelineItemUnknownContent
|
||||
|
||||
fun aTimelineItemStateEventContent() = TimelineItemStateEventContent(
|
||||
body = "A state event",
|
||||
fun aTimelineItemStateEventContent(
|
||||
body: String = "A state event",
|
||||
) = TimelineItemStateEventContent(
|
||||
body = body,
|
||||
)
|
||||
|
||||
@@ -86,11 +86,12 @@ class TimelineItemGrouperTest {
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
TimelineItem.GroupedEvents(
|
||||
computeGroupIdWith(aGroupableItem),
|
||||
id = computeGroupIdWith(aGroupableItem),
|
||||
events = listOf(
|
||||
aGroupableItem.copy("0"),
|
||||
aGroupableItem.copy(id = "1"),
|
||||
).toImmutableList()
|
||||
).toImmutableList(),
|
||||
aggregatedReadReceipts = emptyList<ReadReceiptData>().toImmutableList(),
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -132,20 +133,22 @@ class TimelineItemGrouperTest {
|
||||
assertThat(result).isEqualTo(
|
||||
listOf(
|
||||
TimelineItem.GroupedEvents(
|
||||
computeGroupIdWith(aGroupableItem),
|
||||
id = computeGroupIdWith(aGroupableItem),
|
||||
events = listOf(
|
||||
aGroupableItem,
|
||||
aGroupableItem,
|
||||
).toImmutableList()
|
||||
).toImmutableList(),
|
||||
aggregatedReadReceipts = emptyList<ReadReceiptData>().toImmutableList(),
|
||||
),
|
||||
aNonGroupableItem,
|
||||
TimelineItem.GroupedEvents(
|
||||
computeGroupIdWith(aGroupableItem),
|
||||
id = computeGroupIdWith(aGroupableItem),
|
||||
events = listOf(
|
||||
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
Reference in New Issue
Block a user