Use the new SDK thread info for redacted messages

This commit is contained in:
bxdxnn
2026-08-19 16:12:15 +00:00
parent dfbc75a9e1
commit 2ff6a56e7d
17 changed files with 65 additions and 17 deletions
@@ -91,7 +91,7 @@ internal fun MatrixTimelineItem.Event.canBeDisplayedInBubbleBlock(): Boolean {
// Can be grouped // Can be grouped
is FailedToParseMessageLikeContent, is FailedToParseMessageLikeContent,
is MessageContent, is MessageContent,
RedactedContent, is RedactedContent,
is StickerContent, is StickerContent,
is PollContent, is PollContent,
is UnableToDecryptContent, is UnableToDecryptContent,
@@ -47,7 +47,6 @@ import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
@@ -65,6 +64,7 @@ import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermissions
import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.aMessageContent
import io.element.android.libraries.matrix.test.timeline.aRedactedContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent
import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.matrix.ui.components.aMatrixUserList
@@ -876,7 +876,7 @@ class TimelinePresenterTest {
(0 until 3).map { index -> (0 until 3).map { index ->
MatrixTimelineItem.Event( MatrixTimelineItem.Event(
uniqueId = UniqueId("redacted_$index"), uniqueId = UniqueId("redacted_$index"),
event = anEventTimelineItem(eventId = EventId("\$R$index"), content = RedactedContent), event = anEventTimelineItem(eventId = EventId("\$R$index"), content = aRedactedContent()),
) )
} }
), ),
@@ -12,10 +12,14 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory 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.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.TimelineItemThreadInfo
import io.element.android.libraries.matrix.api.core.UniqueId 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.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.matrix.test.A_USER_ID 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.aMessageContent
import io.element.android.libraries.matrix.test.timeline.aRedactedContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@@ -82,6 +86,42 @@ class TimelineItemsFactoryTest {
).inOrder() ).inOrder()
} }
@Test
fun `redacted message keeps its thread info`() = 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,
timestamp = 0L,
content = aRedactedContent(
threadInfo = EventThreadInfo.ThreadResponse(A_THREAD_ID),
),
),
)
)
var threadInfo: TimelineItemThreadInfo? = null
factory.timelineItems.test {
factory.replaceWith(
timelineItems = items,
roomMembers = emptyList(),
renderReadReceipts = false,
)
threadInfo = awaitItem()
.filterIsInstance<TimelineItem.Event>()
.firstOrNull()
?.threadInfo
cancelAndIgnoreRemainingEvents()
}
assertThat(threadInfo).isEqualTo(TimelineItemThreadInfo.ThreadResponse(A_THREAD_ID))
}
private suspend fun TestScope.groupPositionsOf(timestamps: List<Long>): List<TimelineItemGroupPosition> { private suspend fun TestScope.groupPositionsOf(timestamps: List<Long>): List<TimelineItemGroupPosition> {
val factory = aTimelineItemsFactory( val factory = aTimelineItemsFactory(
config = TimelineItemsFactoryConfig( config = TimelineItemsFactoryConfig(
@@ -16,11 +16,11 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.core.FakeSendHandle import io.element.android.libraries.matrix.test.core.FakeSendHandle
import io.element.android.libraries.matrix.test.timeline.aRedactedContent
import io.element.android.libraries.mediaplayer.api.MediaPlayer import io.element.android.libraries.mediaplayer.api.MediaPlayer
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
@@ -88,7 +88,7 @@ fun aRedactedMatrixTimeline(eventId: EventId) = listOf<MatrixTimelineItem>(
sender = A_USER_ID, sender = A_USER_ID,
senderProfile = ProfileDetails.Unavailable, senderProfile = ProfileDetails.Unavailable,
timestamp = 9442, timestamp = 9442,
content = RedactedContent, content = aRedactedContent(),
origin = null, origin = null,
timelineItemDebugInfoProvider = { timelineItemDebugInfoProvider = {
TimelineItemDebugInfo( TimelineItemDebugInfo(
@@ -56,7 +56,7 @@ class DefaultPinnedMessagesBannerFormatter(
is PollContent -> { is PollContent -> {
content.question.prefixWith(CommonStrings.a11y_poll) content.question.prefixWith(CommonStrings.a11y_poll)
} }
RedactedContent -> { is RedactedContent -> {
sp.getString(CommonStrings.common_message_removed) sp.getString(CommonStrings.common_message_removed)
} }
else -> { else -> {
@@ -89,7 +89,7 @@ class DefaultRoomLatestEventFormatter(
): CharSequence? { ): CharSequence? {
return when (content) { return when (content) {
is MessageContent -> content.process(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) is MessageContent -> content.process(senderDisambiguatedDisplayName, isDmRoom, isOutgoing)
RedactedContent -> { is RedactedContent -> {
val message = sp.getString(CommonStrings.common_message_removed) val message = sp.getString(CommonStrings.common_message_removed)
message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing)
} }
@@ -62,7 +62,7 @@ class DefaultTimelineEventFormatter(
is CallNotifyContent -> { is CallNotifyContent -> {
sp.getString(CommonStrings.common_call_started) sp.getString(CommonStrings.common_call_started)
} }
RedactedContent, is RedactedContent,
is LegacyCallInviteContent, is LegacyCallInviteContent,
is StickerContent, is StickerContent,
is PollContent, is PollContent,
@@ -81,7 +81,7 @@ class DefaultPinnedMessagesBannerFormatterTest : RobolectricTest() {
fun `Redacted content`() { fun `Redacted content`() {
val expected = "Message removed" val expected = "Message removed"
val senderName = "Someone" val senderName = "Someone"
val message = createRoomEvent(false, senderName, RedactedContent) val message = createRoomEvent(false, senderName, RedactedContent(threadInfo = null))
val result = formatter.format(message) val result = formatter.format(message)
assertThat(result).isEqualTo(expected) assertThat(result).isEqualTo(expected)
} }
@@ -85,7 +85,7 @@ class DefaultRoomLatestEventFormatterTest : RobolectricTest() {
val expected = "Message removed" val expected = "Message removed"
val senderName = "Someone" val senderName = "Someone"
sequenceOf(false, true).forEach { isDm -> sequenceOf(false, true).forEach { isDm ->
val message = createLatestEvent(false, senderName, RedactedContent) val message = createLatestEvent(false, senderName, RedactedContent(threadInfo = null))
val result = formatter.format(message, isDm) val result = formatter.format(message, isDm)
if (isDm) { if (isDm) {
assertThat(result).isEqualTo(expected) assertThat(result).isEqualTo(expected)
@@ -32,7 +32,9 @@ data class MessageContent(
val type: MessageType, val type: MessageType,
) : EventContent ) : EventContent
data object RedactedContent : EventContent data class RedactedContent(
val threadInfo: EventThreadInfo?,
) : EventContent
data class StickerContent( data class StickerContent(
val filename: String, val filename: String,
@@ -51,6 +51,7 @@ data class EventTimelineItem(
is PollContent -> content.threadInfo is PollContent -> content.threadInfo
is StickerContent -> content.threadInfo is StickerContent -> content.threadInfo
is UnableToDecryptContent -> content.threadInfo is UnableToDecryptContent -> content.threadInfo
is RedactedContent -> content.threadInfo
else -> null else -> null
} }
@@ -81,7 +81,7 @@ class TimelineEventContentMapper(
) )
} }
is MsgLikeKind.Redacted -> { is MsgLikeKind.Redacted -> {
RedactedContent RedactedContent(threadInfo = extractThreadInfo(it.content))
} }
is MsgLikeKind.Poll -> { is MsgLikeKind.Poll -> {
PollContent( PollContent(
@@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
import io.element.android.libraries.matrix.api.timeline.item.event.SendHandleProvider import io.element.android.libraries.matrix.api.timeline.item.event.SendHandleProvider
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
@@ -137,6 +138,10 @@ fun aStickerContent(
threadInfo = threadInfo, threadInfo = threadInfo,
) )
fun aRedactedContent(
threadInfo: EventThreadInfo? = null,
) = RedactedContent(threadInfo = threadInfo)
fun aTimelineItemDebugInfo( fun aTimelineItemDebugInfo(
model: String = "Rust(Model())", model: String = "Rust(Model())",
originalJson: String? = null, originalJson: String? = null,
@@ -117,7 +117,7 @@ class InReplyToDetailsDisambiguatedProvider : InReplyToDetailsProvider() {
class InReplyToDetailsInformativeProvider : InReplyToDetailsProvider() { class InReplyToDetailsInformativeProvider : InReplyToDetailsProvider() {
override val values: Sequence<InReplyToDetails> override val values: Sequence<InReplyToDetails>
get() = sequenceOf( get() = sequenceOf(
RedactedContent, RedactedContent(threadInfo = null),
UnableToDecryptContent(data = UnableToDecryptContent.Data.Unknown, threadInfo = null), UnableToDecryptContent(data = UnableToDecryptContent.Data.Unknown, threadInfo = null),
).map { ).map {
aInReplyToDetails( aInReplyToDetails(
@@ -656,7 +656,7 @@ class InReplyToMetadataKtTest : RobolectricTest() {
fun `redacted content`() = runTest { fun `redacted content`() = runTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
anInReplyToDetailsReady( anInReplyToDetailsReady(
eventContent = RedactedContent eventContent = RedactedContent(threadInfo = null)
).metadata(hideImage = false) ).metadata(hideImage = false)
}.test { }.test {
awaitItem().let { awaitItem().let {
@@ -79,7 +79,7 @@ class EventItemFactory(
LegacyCallInviteContent, LegacyCallInviteContent,
is PollContent, is PollContent,
is ProfileChangeContent, is ProfileChangeContent,
RedactedContent, is RedactedContent,
is RoomMembershipContent, is RoomMembershipContent,
is StateContent, is StateContent,
is StickerContent, is StickerContent,
@@ -35,7 +35,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessa
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
@@ -49,6 +48,7 @@ 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.aMessageContent
import io.element.android.libraries.matrix.test.timeline.aPollContent import io.element.android.libraries.matrix.test.timeline.aPollContent
import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent
import io.element.android.libraries.matrix.test.timeline.aRedactedContent
import io.element.android.libraries.matrix.test.timeline.aStickerContent import io.element.android.libraries.matrix.test.timeline.aStickerContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent
@@ -80,7 +80,7 @@ class EventItemFactoryTest {
LegacyCallInviteContent, LegacyCallInviteContent,
aPollContent(), aPollContent(),
aProfileChangeMessageContent(), aProfileChangeMessageContent(),
RedactedContent, aRedactedContent(),
aRoomMembershipContent( aRoomMembershipContent(
userId = A_USER_ID, userId = A_USER_ID,
), ),