diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt index 2e01e53828..b34c66a08d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt @@ -12,6 +12,7 @@ import androidx.annotation.VisibleForTesting import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.UniqueId @@ -99,7 +100,8 @@ internal const val MIN_REDACTED_RUN_SIZE = 3 * Fold runs of [MIN_REDACTED_RUN_SIZE] or more consecutive deleted (redacted) messages into a single * [TimelineItem.GroupedEvents], so a long stretch of "Message removed" tiles shows as one expandable * "N removed messages" line, like element-web. Shorter runs, and anything that is not a redacted event - * (day dividers included), are left untouched. + * (day dividers included), are left untouched. A deleted message that still heads a thread is left + * untouched too, so that its thread summary stays reachable. * * This runs as the final step of [TimelineItemGrouper.group], after the state and membership events * have been grouped. It is deliberately kept out of the normal grouping: a run needs a higher minimum @@ -134,7 +136,7 @@ internal fun List.collapseRedactedRuns(groupIds: MutableMap() + .single() + .threadInfo + assertThat(threadInfo).isInstanceOf(TimelineItemThreadInfo.ThreadRoot::class.java) + assertThat((threadInfo as TimelineItemThreadInfo.ThreadRoot).summary.numberOfReplies).isEqualTo(3) + cancelAndIgnoreRemainingEvents() + } + } + private suspend fun TestScope.groupPositionsOf(timestamps: List): List { val factory = aTimelineItemsFactory( config = TimelineItemsFactoryConfig( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt index 005eb5730e..f6ef4f0fc2 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouperTest.kt @@ -15,11 +15,14 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemReactions 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.TimelineItemReadReceipts +import io.element.android.features.messages.impl.timeline.model.TimelineItemThreadInfo import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.anAvatarData import io.element.android.libraries.matrix.api.core.UniqueId +import io.element.android.libraries.matrix.api.timeline.item.ThreadSummary import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_USER_ID @@ -190,6 +193,22 @@ class TimelineItemGrouperTest { ) } + @Test + fun `a deleted message that still heads a thread is left out of the removed messages group`() { + val threadRoot = aGroupableItem.copy( + id = UniqueId("r1"), + content = TimelineItemRedactedContent, + threadInfo = TimelineItemThreadInfo.ThreadRoot( + summary = ThreadSummary(latestEvent = AsyncData.Uninitialized, numberOfReplies = 2), + latestEventText = null, + ), + ) + val r0 = aGroupableItem.copy(id = UniqueId("r0"), content = TimelineItemRedactedContent) + val r2 = aGroupableItem.copy(id = UniqueId("r2"), content = TimelineItemRedactedContent) + val result = sut.group(listOf(r0, threadRoot, r2)) + assertThat(result).isEqualTo(listOf(r0, threadRoot, r2)) + } + @Test fun `a run of fewer than three redacted events is left untouched`() { val r0 = aGroupableItem.copy(id = UniqueId("r0"), content = TimelineItemRedactedContent)