mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Keep the thread of a message that has been deleted
Deleting a message that headed a thread took the thread with it: the "N replies" summary under the message disappeared and there was no way back to the replies, which are still there. Every branch of the timeline content mapper passes `MsgLikeContent`'s thread information on except the redacted one, which returned a bare `RedactedContent` and dropped it. The thread summary lives on `MsgLikeContent` next to `kind`, so it survives the redaction; only the app was throwing it away. `RedactedContent` now carries the thread info like the other contents do, and `EventTimelineItem.threadInfo()` reads it. A deleted message that still heads a thread is also left out of the "N removed messages" group, which would otherwise hide the summary again as soon as three deletions ended up next to each other. Part of #7397
This commit is contained in:
+1
-1
@@ -91,7 +91,7 @@ internal fun MatrixTimelineItem.Event.canBeDisplayedInBubbleBlock(): Boolean {
|
||||
// Can be grouped
|
||||
is FailedToParseMessageLikeContent,
|
||||
is MessageContent,
|
||||
RedactedContent,
|
||||
is RedactedContent,
|
||||
is StickerContent,
|
||||
is PollContent,
|
||||
is UnableToDecryptContent,
|
||||
|
||||
+4
-2
@@ -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<TimelineItem>.collapseRedactedRuns(groupIds: MutableMap<String
|
||||
}
|
||||
|
||||
for (item in this) {
|
||||
if (item is TimelineItem.Event && item.content is TimelineItemRedactedContent) {
|
||||
if (item is TimelineItem.Event && item.content is TimelineItemRedactedContent && item.threadInfo !is TimelineItemThreadInfo.ThreadRoot) {
|
||||
run.add(item)
|
||||
} else {
|
||||
flushRun()
|
||||
|
||||
+1
-1
@@ -880,7 +880,7 @@ class TimelinePresenterTest {
|
||||
(0 until 3).map { index ->
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = UniqueId("redacted_$index"),
|
||||
event = anEventTimelineItem(eventId = EventId("\$R$index"), content = RedactedContent),
|
||||
event = anEventTimelineItem(eventId = EventId("\$R$index"), content = RedactedContent(threadInfo = null)),
|
||||
)
|
||||
}
|
||||
),
|
||||
|
||||
+42
@@ -12,10 +12,15 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory
|
||||
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.TimelineItemThreadInfo
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
|
||||
import io.element.android.libraries.matrix.api.timeline.item.ThreadSummary
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.timeline.aMessageContent
|
||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||
@@ -128,6 +133,43 @@ class TimelineItemsFactoryTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a deleted message keeps the summary of the thread it heads`() = runTest {
|
||||
val factory = aTimelineItemsFactory(
|
||||
config = TimelineItemsFactoryConfig(
|
||||
computeReadReceipts = false,
|
||||
computeReactions = false,
|
||||
)
|
||||
)
|
||||
val items = listOf(
|
||||
MatrixTimelineItem.Event(
|
||||
uniqueId = UniqueId("event-0"),
|
||||
event = anEventTimelineItem(
|
||||
sender = A_USER_ID,
|
||||
content = RedactedContent(
|
||||
threadInfo = EventThreadInfo.ThreadRoot(
|
||||
summary = ThreadSummary(latestEvent = AsyncData.Uninitialized, numberOfReplies = 3),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
factory.timelineItems.test {
|
||||
factory.replaceWith(
|
||||
timelineItems = items,
|
||||
roomMembers = emptyList(),
|
||||
renderReadReceipts = false,
|
||||
)
|
||||
val threadInfo = awaitItem()
|
||||
.filterIsInstance<TimelineItem.Event>()
|
||||
.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<Long>): List<TimelineItemGroupPosition> {
|
||||
val factory = aTimelineItemsFactory(
|
||||
config = TimelineItemsFactoryConfig(
|
||||
|
||||
+19
@@ -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)
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ fun aRedactedMatrixTimeline(eventId: EventId) = listOf<MatrixTimelineItem>(
|
||||
sender = A_USER_ID,
|
||||
senderProfile = ProfileDetails.Unavailable,
|
||||
timestamp = 9442,
|
||||
content = RedactedContent,
|
||||
content = RedactedContent(threadInfo = null),
|
||||
origin = null,
|
||||
timelineItemDebugInfoProvider = {
|
||||
TimelineItemDebugInfo(
|
||||
|
||||
Reference in New Issue
Block a user