Read receipt: Rework how the feature flag is used.

tom
This commit is contained in:
Benoit Marty 2023-11-17 16:40:15 +01:00 committed by Benoit Marty
parent c8b8938aa8
commit 014b15771b
12 changed files with 59 additions and 46 deletions

View file

@ -147,9 +147,10 @@ class TimelinePresenter @Inject constructor(
timelineItemsFactory.replaceWith(
timelineItems = it,
roomMembers = if (readReceiptsEnabled) {
membersState.roomMembers()
membersState.roomMembers().orEmpty()
} else {
null
// Give an empty list to not affect performance
emptyList()
}
)
}
@ -166,6 +167,7 @@ class TimelinePresenter @Inject constructor(
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
paginationState = paginationState,
timelineItems = timelineItems,
showReadReceipts = readReceiptsEnabled,
hasNewItems = hasNewItems.value,
sessionState = sessionState,
eventSink = ::handleEvents

View file

@ -26,6 +26,7 @@ import kotlinx.collections.immutable.ImmutableList
@Immutable
data class TimelineState(
val timelineItems: ImmutableList<TimelineItem>,
val showReadReceipts: Boolean,
val highlightedEventId: EventId?,
val userHasPermissionToSendMessage: Boolean,
val paginationState: MatrixTimeline.PaginationState,

View file

@ -16,6 +16,7 @@
package io.element.android.features.messages.impl.timeline
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
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.TimelineItemReactions
@ -44,6 +45,7 @@ import kotlin.random.Random
fun aTimelineState(timelineItems: ImmutableList<TimelineItem> = persistentListOf()) = TimelineState(
timelineItems = timelineItems,
showReadReceipts = false,
paginationState = MatrixTimeline.PaginationState(
isBackPaginating = false,
hasMoreToLoadBackwards = true,
@ -124,7 +126,7 @@ internal fun aTimelineItemEvent(
isThreaded: Boolean = false,
debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(),
timelineItemReactions: TimelineItemReactions = aTimelineItemReactions(),
readReceiptState: TimelineItemReadReceipts = TimelineItemReadReceipts.Hidden,
readReceiptState: TimelineItemReadReceipts = aTimelineItemReadReceipts(),
): TimelineItem.Event {
return TimelineItem.Event(
id = UUID.randomUUID().toString(),
@ -176,6 +178,12 @@ internal fun aTimelineItemDebugInfo(
model, originalJson, latestEditedJson
)
internal fun aTimelineItemReadReceipts(): TimelineItemReadReceipts {
return TimelineItemReadReceipts(
receipts = emptyList<ReadReceiptData>().toImmutableList(),
)
}
fun aGroupedEvents(id: Long = 0): TimelineItem.GroupedEvents {
val event = aTimelineItemEvent(
isMine = true,

View file

@ -127,6 +127,7 @@ fun TimelineView(
) { timelineItem ->
TimelineItemRow(
timelineItem = timelineItem,
showReadReceipts = state.showReadReceipts,
highlightedItem = state.highlightedEventId?.value,
userHasPermissionToSendMessage = state.userHasPermissionToSendMessage,
onClick = onMessageClicked,
@ -171,6 +172,7 @@ fun TimelineView(
@Composable
private fun TimelineItemRow(
timelineItem: TimelineItem,
showReadReceipts: Boolean,
highlightedItem: String?,
userHasPermissionToSendMessage: Boolean,
sessionState: SessionState,
@ -208,6 +210,7 @@ private fun TimelineItemRow(
} else {
TimelineItemEventRow(
event = timelineItem,
showReadReceipts = showReadReceipts,
isHighlighted = highlightedItem == timelineItem.identifier(),
canReply = userHasPermissionToSendMessage && timelineItem.content.canBeRepliedTo(),
onClick = { onClick(timelineItem) },
@ -248,6 +251,7 @@ private fun TimelineItemRow(
timelineItem.events.forEach { subGroupEvent ->
TimelineItemRow(
timelineItem = subGroupEvent,
showReadReceipts = showReadReceipts,
highlightedItem = highlightedItem,
sessionState = sessionState,
userHasPermissionToSendMessage = false,

View file

@ -79,7 +79,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.features.messages.impl.timeline.model.receipts
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
import io.element.android.libraries.designsystem.components.EqualWidthColumn
import io.element.android.libraries.designsystem.components.avatar.Avatar
@ -117,6 +116,7 @@ import kotlin.math.roundToInt
@Composable
fun TimelineItemEventRow(
event: TimelineItem.Event,
showReadReceipts: Boolean,
isHighlighted: Boolean,
canReply: Boolean,
onClick: () -> Unit,
@ -177,6 +177,7 @@ fun TimelineItemEventRow(
state = state.draggableState,
),
event = event,
showReadReceipts = showReadReceipts,
isHighlighted = isHighlighted,
interactionSource = interactionSource,
onClick = onClick,
@ -195,6 +196,7 @@ fun TimelineItemEventRow(
} else {
TimelineItemEventRowContent(
event = event,
showReadReceipts = showReadReceipts,
isHighlighted = isHighlighted,
interactionSource = interactionSource,
onClick = onClick,
@ -238,6 +240,7 @@ private fun SwipeSensitivity(
@Composable
private fun TimelineItemEventRowContent(
event: TimelineItem.Event,
showReadReceipts: Boolean,
isHighlighted: Boolean,
interactionSource: MutableInteractionSource,
onClick: () -> Unit,
@ -336,21 +339,23 @@ private fun TimelineItemEventRowContent(
}
// Read receipts / Send state
TimelineItemReadReceiptView(
state = ReadReceiptViewState(
sendState = event.localSendState,
receipts = event.readReceiptState.receipts(),
),
onReadReceiptsClicked = onReadReceiptsClicked,
modifier = Modifier
.constrainAs(readReceipts) {
if (event.reactionsState.reactions.isNotEmpty()) {
top.linkTo(reactions.bottom, margin = 4.dp)
} else {
top.linkTo(message.bottom, margin = 4.dp)
if (showReadReceipts) {
TimelineItemReadReceiptView(
state = ReadReceiptViewState(
sendState = event.localSendState,
receipts = event.readReceiptState.receipts,
),
onReadReceiptsClicked = onReadReceiptsClicked,
modifier = Modifier
.constrainAs(readReceipts) {
if (event.reactionsState.reactions.isNotEmpty()) {
top.linkTo(reactions.bottom, margin = 4.dp)
} else {
top.linkTo(message.bottom, margin = 4.dp)
}
}
}
)
)
}
}
}
@ -679,6 +684,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
),
groupPosition = TimelineItemGroupPosition.First,
),
showReadReceipts = false,
isHighlighted = false,
canReply = true,
onClick = {},
@ -701,6 +707,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
),
groupPosition = TimelineItemGroupPosition.Last,
),
showReadReceipts = false,
isHighlighted = false,
canReply = true,
onClick = {},
@ -741,6 +748,7 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview {
inReplyTo = aInReplyToReady(replyContent),
groupPosition = TimelineItemGroupPosition.First,
),
showReadReceipts = false,
isHighlighted = false,
canReply = true,
onClick = {},
@ -765,6 +773,7 @@ internal fun TimelineItemEventRowWithReplyPreview() = ElementPreview {
isThreaded = true,
groupPosition = TimelineItemGroupPosition.Last,
),
showReadReceipts = false,
isHighlighted = false,
canReply = true,
onClick = {},
@ -817,6 +826,7 @@ internal fun TimelineItemEventRowTimestampPreview(
reactionsState = aTimelineItemReactions(count = 0),
senderDisplayName = if (useDocument) "Document case" else "Text case",
),
showReadReceipts = false,
isHighlighted = false,
canReply = true,
onClick = {},
@ -850,6 +860,7 @@ internal fun TimelineItemEventRowWithManyReactionsPreview() = ElementPreview {
),
timelineItemReactions = aTimelineItemReactions(count = 20),
),
showReadReceipts = false,
isHighlighted = false,
canReply = true,
onClick = {},
@ -876,6 +887,7 @@ internal fun TimelineItemEventRowLongSenderNamePreview() = ElementPreviewLight {
event = aTimelineItemEvent(
senderDisplayName = "a long sender display name to test single line and ellipsis at the end of the line",
),
showReadReceipts = false,
isHighlighted = false,
canReply = true,
onClick = {},
@ -898,6 +910,7 @@ internal fun TimelineItemEventRowLongSenderNamePreview() = ElementPreviewLight {
internal fun TimelineItemEventTimestampBelowPreview() = ElementPreviewLight {
TimelineItemEventRow(
event = aTimelineItemEvent(content = aTimelineItemPollContent()),
showReadReceipts = false,
isHighlighted = false,
canReply = true,
onClick = {},

View file

@ -29,7 +29,6 @@ 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.features.messages.impl.timeline.model.receipts
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
@ -93,7 +92,7 @@ private fun ColumnScope.ReadReceiptBottomSheetContent(
Text(text = stringResource(id = CommonStrings.common_seen_by))
}
)
val receipts = state.selectedEvent?.readReceiptState?.receipts().orEmpty()
val receipts = state.selectedEvent?.readReceiptState?.receipts.orEmpty()
receipts.forEach {
val userId = UserId(it.avatarData.id)
MatrixUserRow(

View file

@ -31,7 +31,7 @@ class ReadReceiptBottomSheetStateProvider : PreviewParameterProvider<ReadReceipt
.map { readReceiptViewState ->
ReadReceiptBottomSheetState(
selectedEvent = aTimelineItemEvent(
readReceiptState = TimelineItemReadReceipts.ReadReceipts(
readReceiptState = TimelineItemReadReceipts(
receipts = readReceiptViewState.receipts.map { readReceiptData ->
readReceiptData
.copy(avatarData = readReceiptData.avatarData.copy(id = "@${readReceiptData.avatarData.id}:localhost"))

View file

@ -67,7 +67,7 @@ class TimelineItemsFactory @Inject constructor(
suspend fun replaceWith(
timelineItems: List<MatrixTimelineItem>,
roomMembers: List<RoomMember>?,
roomMembers: List<RoomMember>,
) = withContext(dispatchers.computation) {
lock.withLock {
diffCacheUpdater.updateWith(timelineItems)
@ -77,7 +77,7 @@ class TimelineItemsFactory @Inject constructor(
private suspend fun buildAndEmitTimelineItemStates(
timelineItems: List<MatrixTimelineItem>,
roomMembers: List<RoomMember>?,
roomMembers: List<RoomMember>,
) {
val newTimelineItemStates = ArrayList<TimelineItem>()
for (index in diffCache.indices().reversed()) {
@ -97,7 +97,7 @@ class TimelineItemsFactory @Inject constructor(
private suspend fun buildAndCacheItem(
timelineItems: List<MatrixTimelineItem>,
index: Int,
roomMembers: List<RoomMember>?,
roomMembers: List<RoomMember>,
): TimelineItem? {
val timelineItemState =
when (val currentTimelineItem = timelineItems[index]) {

View file

@ -47,7 +47,7 @@ class TimelineItemEventFactory @Inject constructor(
currentTimelineItem: MatrixTimelineItem.Event,
index: Int,
timelineItems: List<MatrixTimelineItem>,
roomMembers: List<RoomMember>?,
roomMembers: List<RoomMember>,
): TimelineItem.Event {
val currentSender = currentTimelineItem.event.sender
val groupPosition =
@ -132,10 +132,9 @@ class TimelineItemEventFactory @Inject constructor(
}
private fun MatrixTimelineItem.Event.computeReadReceiptState(
roomMembers: List<RoomMember>?,
roomMembers: List<RoomMember>,
): TimelineItemReadReceipts {
if (roomMembers == null) return TimelineItemReadReceipts.Hidden
return TimelineItemReadReceipts.ReadReceipts(
return TimelineItemReadReceipts(
receipts = event.receipts
.map { receipt ->
val roomMember = roomMembers.find { it.userId == receipt.userId }

View file

@ -18,25 +18,12 @@ package io.element.android.features.messages.impl.timeline.model
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
sealed interface TimelineItemReadReceipts {
/**
* Value when the feature is disabled.
*/
data object Hidden : TimelineItemReadReceipts
data class ReadReceipts(
val receipts: ImmutableList<ReadReceiptData>,
) : TimelineItemReadReceipts
}
data class TimelineItemReadReceipts(
val receipts: ImmutableList<ReadReceiptData>,
)
data class ReadReceiptData(
val avatarData: AvatarData,
val formattedDate: String,
)
fun TimelineItemReadReceipts.receipts(): ImmutableList<ReadReceiptData> = when (this) {
TimelineItemReadReceipts.Hidden -> persistentListOf()
is TimelineItemReadReceipts.ReadReceipts -> receipts
}

View file

@ -53,7 +53,7 @@ internal fun aMessageEvent(
sentTime = "",
isMine = isMine,
reactionsState = aTimelineItemReactions(count = 0),
readReceiptState = TimelineItemReadReceipts.ReadReceipts(emptyList<ReadReceiptData>().toImmutableList()),
readReceiptState = TimelineItemReadReceipts(emptyList<ReadReceiptData>().toImmutableList()),
localSendState = sendState,
inReplyTo = inReplyTo,
debugInfo = debugInfo,

View file

@ -44,7 +44,7 @@ class TimelineItemGrouperTest {
senderDisplayName = "",
content = TimelineItemStateEventContent(body = "a state event"),
reactionsState = aTimelineItemReactions(count = 0),
readReceiptState = TimelineItemReadReceipts.ReadReceipts(emptyList<ReadReceiptData>().toImmutableList()),
readReceiptState = TimelineItemReadReceipts(emptyList<ReadReceiptData>().toImmutableList()),
localSendState = LocalEventSendState.Sent(AN_EVENT_ID),
inReplyTo = null,
isThreaded = false,