From 1e67c2f77bd3177a490528f8bc092c53bad806cc Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 26 May 2026 10:28:32 +0200 Subject: [PATCH] Move empty day separator filtering to a timeline post-processor (#6866) * Move empty day separator filtering to a timeline post-processor * Split `FilterPublicMembershipChangesPostProcessor` from `RoomBeginningPostProcessor` --- .../factories/TimelineItemsFactory.kt | 26 +-- .../factories/TimelineItemsFactoryTest.kt | 160 ------------------ .../matrix/impl/timeline/RustTimeline.kt | 20 ++- .../FilterEmptyDayPostProcessor.kt | 38 +++++ ...terPublicMembershipChangesPostProcessor.kt | 43 +++++ .../RoomBeginningPostProcessor.kt | 16 -- .../FilterEmptyDayPostProcessorTest.kt | 118 +++++++++++++ ...ublicMembershipChangesPostProcessorTest.kt | 75 ++++++++ .../RoomBeginningPostProcessorTest.kt | 89 ---------- 9 files changed, 292 insertions(+), 293 deletions(-) delete mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactoryTest.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterEmptyDayPostProcessor.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterPublicMembershipChangesPostProcessor.kt create mode 100644 libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterEmptyDayPostProcessorTest.kt create mode 100644 libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterPublicMembershipChangesPostProcessorTest.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt index dc8bdddc92..7b369fe6b7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt @@ -16,7 +16,6 @@ import io.element.android.features.messages.impl.timeline.factories.event.Timeli import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper import io.element.android.features.messages.impl.timeline.model.TimelineItem -import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel import io.element.android.libraries.androidutils.diff.DiffCacheUpdater import io.element.android.libraries.androidutils.diff.MutableListDiffCache import io.element.android.libraries.core.coroutine.CoroutineDispatchers @@ -97,8 +96,7 @@ class TimelineItemsFactory( } } val result = timelineItemGrouper.group(newTimelineItemStates).toImmutableList() - val filteredResult = filterEmptyDaySeparators(result) - this._timelineItems.emit(filteredResult) + this._timelineItems.emit(result) } private suspend fun buildAndCacheItem( @@ -116,25 +114,3 @@ class TimelineItemsFactory( return timelineItem } } - -// Remove day separators for days with no events after the client-side event filtering -internal fun filterEmptyDaySeparators(items: List): ImmutableList { - return buildList { - var hasEventBefore = false - for (item in items) { - when (item) { - is TimelineItem.Event, is TimelineItem.GroupedEvents -> { - hasEventBefore = true - add(item) - } - is TimelineItem.Virtual if item.model is TimelineItemDaySeparatorModel -> { - if (hasEventBefore) { - add(item) - } - hasEventBefore = false - } - else -> add(item) - } - } - }.toImmutableList() -} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactoryTest.kt deleted file mode 100644 index 8df8d34f56..0000000000 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactoryTest.kt +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.messages.impl.timeline.factories - -import com.google.common.truth.Truth.assertThat -import io.element.android.features.messages.impl.fixtures.aMessageEvent -import io.element.android.features.messages.impl.timeline.aTimelineItemDebugInfo -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.virtual.TimelineItemReadMarkerModel -import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel -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.event.LocalEventSendState -import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails -import io.element.android.libraries.matrix.test.AN_EVENT_ID -import io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.matrix.test.core.FakeSendHandle -import kotlinx.collections.immutable.toImmutableList -import org.junit.Test - -class TimelineItemsFactoryTest { - private val anEvent = TimelineItem.Event( - id = UniqueId("event"), - eventId = AN_EVENT_ID, - senderId = A_USER_ID, - senderAvatar = anAvatarData(), - senderProfile = ProfileDetails.Ready(displayName = "User", displayNameAmbiguous = false, avatarUrl = null), - content = aMessageEvent().content, - reactionsState = aTimelineItemReactions(count = 0), - readReceiptState = TimelineItemReadReceipts(emptyList().toImmutableList()), - localSendState = LocalEventSendState.Sent(AN_EVENT_ID), - isEditable = false, - canBeRepliedTo = false, - inReplyTo = null, - threadInfo = null, - origin = null, - timelineItemDebugInfoProvider = { aTimelineItemDebugInfo() }, - messageShieldProvider = { null }, - sendHandleProvider = { FakeSendHandle() }, - forwarder = null, - forwarderProfile = null, - ) - - private fun aDaySeparator(date: String) = TimelineItem.Virtual( - id = UniqueId("day_$date"), - model = aTimelineItemDaySeparatorModel(date) - ) - - @Test - fun `filterEmptyDaySeparators keeps day separator with events after it`() { - val items = listOf( - anEvent, - aDaySeparator("Today"), - ) - val result = filterEmptyDaySeparators(items) - assertThat(result).hasSize(2) - assertThat(result[0]).isEqualTo(anEvent) - assertThat(result[1]).isEqualTo(aDaySeparator("Today")) - } - - @Test - fun `filterEmptyDaySeparators removes day separator with no events after it`() { - val items = listOf( - aDaySeparator("Today"), - aDaySeparator("Yesterday"), - ) - val result = filterEmptyDaySeparators(items) - assertThat(result).isEmpty() - } - - @Test - fun `filterEmptyDaySeparators removes first day separator and keeps second when only second has events`() { - val items = listOf( - aDaySeparator("Today"), - anEvent, - aDaySeparator("Yesterday"), - ) - val result = filterEmptyDaySeparators(items) - assertThat(result).hasSize(2) - assertThat(result[0]).isEqualTo(anEvent) - assertThat(result[1]).isEqualTo(aDaySeparator("Yesterday")) - } - - @Test - fun `filterEmptyDaySeparators handles multiple day separators in a row with no events`() { - val items = listOf( - aDaySeparator("Today"), - aDaySeparator("Yesterday"), - aDaySeparator("Last week"), - ) - val result = filterEmptyDaySeparators(items) - assertThat(result).isEmpty() - } - - @Test - fun `filterEmptyDaySeparators keeps all items when no day separators`() { - val items = listOf( - anEvent, - anEvent.copy(id = UniqueId("event2")), - ) - val result = filterEmptyDaySeparators(items) - assertThat(result).hasSize(2) - } - - @Test - fun `filterEmptyDaySeparators handles grouped events after day separator`() { - val groupedEvents = TimelineItem.GroupedEvents( - id = UniqueId("grouped"), - events = listOf(anEvent).toImmutableList(), - aggregatedReadReceipts = emptyList().toImmutableList(), - ) - val items = listOf( - groupedEvents, - aDaySeparator("Today"), - ) - val result = filterEmptyDaySeparators(items) - assertThat(result).hasSize(2) - assertThat(result[0]).isEqualTo(groupedEvents) - assertThat(result[1]).isEqualTo(aDaySeparator("Today")) - } - - @Test - fun `filterEmptyDaySeparators removes day separator followed by non-event virtual item`() { - val readMarker = TimelineItem.Virtual( - id = UniqueId("readMarker"), - model = TimelineItemReadMarkerModel - ) - val items = listOf( - aDaySeparator("Today"), - readMarker, - ) - val result = filterEmptyDaySeparators(items) - assertThat(result).hasSize(1) - assertThat(result[0]).isEqualTo(readMarker) - } - - @Test - fun `filterEmptyDaySeparators keeps day separator when non-event virtual items are between separator and event`() { - val readMarker = TimelineItem.Virtual( - id = UniqueId("readMarker"), - model = TimelineItemReadMarkerModel - ) - val items = listOf( - anEvent, - readMarker, - aDaySeparator("Today"), - ) - val result = filterEmptyDaySeparators(items) - assertThat(result).hasSize(3) - assertThat(result[2]).isEqualTo(aDaySeparator("Today")) - } -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt index 016204e1b1..d44676d34a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimeline.kt @@ -38,6 +38,8 @@ import io.element.android.libraries.matrix.impl.room.location.into import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper +import io.element.android.libraries.matrix.impl.timeline.postprocessor.FilterEmptyDayPostProcessor +import io.element.android.libraries.matrix.impl.timeline.postprocessor.FilterPublicMembershipChangesPostProcessor import io.element.android.libraries.matrix.impl.timeline.postprocessor.LastForwardIndicatorsPostProcessor import io.element.android.libraries.matrix.impl.timeline.postprocessor.LoadingIndicatorsPostProcessor import io.element.android.libraries.matrix.impl.timeline.postprocessor.RoomBeginningPostProcessor @@ -84,7 +86,7 @@ private const val PAGINATION_SIZE = 50 class RustTimeline( private val inner: InnerTimeline, override val mode: Timeline.Mode, - private val systemClock: SystemClock, + systemClock: SystemClock, private val joinedRoom: JoinedRoom, private val coroutineScope: CoroutineScope, private val dispatcher: CoroutineDispatcher, @@ -123,6 +125,8 @@ class RustTimeline( private val loadingIndicatorsPostProcessor = LoadingIndicatorsPostProcessor(systemClock) private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(mode) private val typingNotificationPostProcessor = TypingNotificationPostProcessor(mode) + private val publicMembershipChangesPostProcessor = FilterPublicMembershipChangesPostProcessor() + private val emptyDayPostProcessor = FilterEmptyDayPostProcessor() private data class RoomTimelineInfo( val roomCreators: ImmutableList, @@ -245,11 +249,21 @@ class RustTimeline( items = items, isDm = isDm, roomCreator = roomCreators.firstOrNull(), - joinRule = joinRule, - isEncrypted = isEncrypted, hasMoreToLoadBackwards = backwardPaginationStatus.hasMoreToLoad, ) } + // This should be the first post processor after room beginning. + .let { items -> + publicMembershipChangesPostProcessor.process( + items = items, + joinRule = joinRule, + isEncrypted = isEncrypted, + ) + } + // After removing public membership changes, we might end up with empty days, so we need to filter them out. + .let { items -> + emptyDayPostProcessor.process(items) + } .let { items -> loadingIndicatorsPostProcessor.process( items = items, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterEmptyDayPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterEmptyDayPostProcessor.kt new file mode 100644 index 0000000000..b7dceafa25 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterEmptyDayPostProcessor.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.timeline.postprocessor + +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem + +/** + * Post-processor to filter out day separators for days that don't contain any events. + */ +class FilterEmptyDayPostProcessor { + /** + * Filters out day separators from [items] for days that don't contain any events. + */ + fun process(items: List): List = buildList { + // The timeline is ordered by descending timestamp, so events that happened during a day appear before the day separator for that day. + // We can use this to determine if a day separator should be kept or not. + var hasEvent = false + for (item in items) { + if (item is MatrixTimelineItem.Event) { + hasEvent = true + add(item) + } else if (item is MatrixTimelineItem.Virtual && item.virtual is VirtualTimelineItem.DayDivider) { + if (hasEvent) { + add(item) + hasEvent = false + } + } else { + add(item) + } + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterPublicMembershipChangesPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterPublicMembershipChangesPostProcessor.kt new file mode 100644 index 0000000000..806c9369bf --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterPublicMembershipChangesPostProcessor.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.timeline.postprocessor + +import io.element.android.libraries.matrix.api.room.join.JoinRule +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange +import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent +import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent + +/** + * Post-processor to filter out public membership changes for non-encrypted, publicly joinable rooms. + */ +class FilterPublicMembershipChangesPostProcessor { + /** + * Filters out public membership changes from [items] if the room is publicly joinable and not encrypted. + */ + fun process( + items: List, + joinRule: JoinRule?, + isEncrypted: Boolean?, + ): List { + return if (joinRule !is JoinRule.Invite && isEncrypted == false) { + filterMembershipEvents(items) + } else { + items + } + } + + private fun filterMembershipEvents(items: List): List = items.filter { item -> + val eventContent = (item as? MatrixTimelineItem.Event)?.event?.content ?: return@filter true + when (eventContent) { + is RoomMembershipContent -> eventContent.change != null && eventContent.change !in listOf(MembershipChange.JOINED, MembershipChange.LEFT) + is ProfileChangeContent -> false + else -> true + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt index ec6a7a5380..8991d26f9c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessor.kt @@ -9,12 +9,10 @@ package io.element.android.libraries.matrix.impl.timeline.postprocessor import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.matrix.api.timeline.item.event.OtherState -import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent @@ -27,31 +25,17 @@ class RoomBeginningPostProcessor(private val mode: Timeline.Mode) { items: List, isDm: Boolean, roomCreator: UserId?, - joinRule: JoinRule?, - isEncrypted: Boolean?, hasMoreToLoadBackwards: Boolean, ): List { return when { items.isEmpty() -> items mode == Timeline.Mode.PinnedEvents -> items - joinRule !is JoinRule.Invite && isEncrypted == false -> filterRoomMemberEvents(items) isDm -> processForDM(items, roomCreator) hasMoreToLoadBackwards -> items else -> processForRoom(items) } } - private fun filterRoomMemberEvents(items: List): List { - return items.filter { item -> - val eventContent = (item as? MatrixTimelineItem.Event)?.event?.content - when (eventContent) { - is RoomMembershipContent -> eventContent.change !in listOf(MembershipChange.JOINED, MembershipChange.LEFT) - is ProfileChangeContent -> false - else -> true - } - } - } - private fun processForRoom(items: List): List { // No changes needed, timeline start item is already added by the SDK return items diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterEmptyDayPostProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterEmptyDayPostProcessorTest.kt new file mode 100644 index 0000000000..7a3a091f4e --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterEmptyDayPostProcessorTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.timeline.postprocessor + +import com.google.common.truth.Truth.assertThat +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.virtual.VirtualTimelineItem +import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem +import org.junit.Test + +private const val TODAY = 1_779_779_967_000 +private const val YESTERDAY = TODAY - 24 * 60 * 60 * 1000 +private const val DAY_BEFORE_YESTERDAY = YESTERDAY - 24 * 60 * 60 * 1000 + +class FilterEmptyDayPostProcessorTest { + private val anEvent = MatrixTimelineItem.Event( + uniqueId = UniqueId("event"), + event = anEventTimelineItem(), + ) + + private fun aDaySeparator(timestmap: Long) = MatrixTimelineItem.Virtual( + uniqueId = UniqueId("day_$timestmap"), + virtual = VirtualTimelineItem.DayDivider(timestmap) + ) + + @Test + fun `filterEmptyDaySeparators keeps day separator with events after it`() { + val items = listOf( + anEvent, + aDaySeparator(TODAY), + ) + val result = FilterEmptyDayPostProcessor().process(items) + assertThat(result).hasSize(2) + assertThat(result[0]).isEqualTo(anEvent) + assertThat(result[1]).isEqualTo(aDaySeparator(TODAY)) + } + + @Test + fun `filterEmptyDaySeparators removes day separator with no events after it`() { + val items = listOf( + aDaySeparator(TODAY), + aDaySeparator(YESTERDAY), + ) + val result = FilterEmptyDayPostProcessor().process(items) + assertThat(result).isEmpty() + } + + @Test + fun `filterEmptyDaySeparators removes first day separator and keeps second when only second has events`() { + val items = listOf( + aDaySeparator(TODAY), + anEvent, + aDaySeparator(YESTERDAY), + ) + val result = FilterEmptyDayPostProcessor().process(items) + assertThat(result).hasSize(2) + assertThat(result[0]).isEqualTo(anEvent) + assertThat(result[1]).isEqualTo(aDaySeparator(YESTERDAY)) + } + + @Test + fun `filterEmptyDaySeparators handles multiple day separators in a row with no events`() { + val items = listOf( + aDaySeparator(TODAY), + aDaySeparator(YESTERDAY), + aDaySeparator(DAY_BEFORE_YESTERDAY), + ) + val result = FilterEmptyDayPostProcessor().process(items) + assertThat(result).isEmpty() + } + + @Test + fun `filterEmptyDaySeparators keeps all items when no day separators`() { + val items = listOf( + anEvent, + anEvent.copy(uniqueId = UniqueId("event2")), + ) + val result = FilterEmptyDayPostProcessor().process(items) + assertThat(result).hasSize(2) + } + + @Test + fun `filterEmptyDaySeparators removes day separator followed by non-event virtual item`() { + val readMarker = MatrixTimelineItem.Virtual( + uniqueId = UniqueId("readMarker"), + virtual = VirtualTimelineItem.ReadMarker + ) + val items = listOf( + aDaySeparator(TODAY), + readMarker, + ) + val result = FilterEmptyDayPostProcessor().process(items) + assertThat(result).hasSize(1) + assertThat(result[0]).isEqualTo(readMarker) + } + + @Test + fun `filterEmptyDaySeparators keeps day separator when non-event virtual items are between separator and event`() { + val readMarker = MatrixTimelineItem.Virtual( + uniqueId = UniqueId("readMarker"), + virtual = VirtualTimelineItem.ReadMarker + ) + val items = listOf( + anEvent, + readMarker, + aDaySeparator(TODAY), + ) + val result = FilterEmptyDayPostProcessor().process(items) + assertThat(result).hasSize(3) + assertThat(result[2]).isEqualTo(aDaySeparator(TODAY)) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterPublicMembershipChangesPostProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterPublicMembershipChangesPostProcessorTest.kt new file mode 100644 index 0000000000..dd899baf5b --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterPublicMembershipChangesPostProcessorTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.timeline.postprocessor + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.room.join.JoinRule +import org.junit.Test + +class FilterPublicMembershipChangesPostProcessorTest { + @Test + fun `processor removes join, leave, and profile events in unencrypted public rooms`() { + val timelineItems = listOf( + roomCreateEvent, + roomCreatorJoinEvent, + otherMemberJoinEvent, + messageEvent, + otherMemberLeaveEvent, + profileChangeEvent, + ) + val expected = listOf( + roomCreateEvent, + messageEvent, + ) + val processor = FilterPublicMembershipChangesPostProcessor() + val processedItems = processor.process( + timelineItems, + joinRule = JoinRule.Public, + isEncrypted = false, + ) + assertThat(processedItems).isEqualTo(expected) + } + + @Test + fun `processor keeps all events in encrypted public rooms`() { + val timelineItems = listOf( + roomCreateEvent, + roomCreatorJoinEvent, + otherMemberJoinEvent, + messageEvent, + otherMemberLeaveEvent, + profileChangeEvent, + ) + val processor = FilterPublicMembershipChangesPostProcessor() + val processedItems = processor.process( + timelineItems, + joinRule = JoinRule.Public, + isEncrypted = true, + ) + assertThat(processedItems).isEqualTo(timelineItems) + } + + @Test + fun `processor keeps membership events in invite-only rooms`() { + val timelineItems = listOf( + roomCreateEvent, + roomCreatorJoinEvent, + otherMemberJoinEvent, + messageEvent, + otherMemberLeaveEvent, + profileChangeEvent, + ) + val processor = FilterPublicMembershipChangesPostProcessor() + val processedItems = processor.process( + timelineItems, + joinRule = JoinRule.Invite, + isEncrypted = null, + ) + assertThat(processedItems).isEqualTo(timelineItems) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt index b562847d94..680422827c 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/RoomBeginningPostProcessorTest.kt @@ -9,7 +9,6 @@ package io.element.android.libraries.matrix.impl.timeline.postprocessor import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.test.A_USER_ID import org.junit.Test @@ -22,8 +21,6 @@ class RoomBeginningPostProcessorTest { items = emptyList(), isDm = true, roomCreator = A_USER_ID, - joinRule = null, - isEncrypted = null, hasMoreToLoadBackwards = false, ) assertThat(processedItems).isEmpty() @@ -36,8 +33,6 @@ class RoomBeginningPostProcessorTest { items = listOf(messageEvent), isDm = true, roomCreator = A_USER_ID, - joinRule = null, - isEncrypted = null, hasMoreToLoadBackwards = false, ) assertThat(processedItems).isEqualTo(listOf(messageEvent)) @@ -50,8 +45,6 @@ class RoomBeginningPostProcessorTest { items = listOf(messageEvent), isDm = true, roomCreator = null, - joinRule = null, - isEncrypted = null, hasMoreToLoadBackwards = false, ) assertThat(processedItems).isEqualTo(listOf(messageEvent)) @@ -69,8 +62,6 @@ class RoomBeginningPostProcessorTest { items = timelineItems, isDm = true, roomCreator = A_USER_ID, - joinRule = null, - isEncrypted = null, hasMoreToLoadBackwards = false, ) assertThat(processedItems).containsExactly(timelineStartEvent) @@ -87,8 +78,6 @@ class RoomBeginningPostProcessorTest { items = timelineItems, isDm = true, roomCreator = A_USER_ID, - joinRule = null, - isEncrypted = null, hasMoreToLoadBackwards = false, ) assertThat(processedItems).isEqualTo(timelineItems) @@ -111,8 +100,6 @@ class RoomBeginningPostProcessorTest { timelineItems, isDm = true, roomCreator = A_USER_ID, - joinRule = null, - isEncrypted = null, hasMoreToLoadBackwards = false ) assertThat(processedItems).isEqualTo(expected) @@ -129,8 +116,6 @@ class RoomBeginningPostProcessorTest { timelineItems, isDm = true, roomCreator = A_USER_ID, - joinRule = null, - isEncrypted = null, hasMoreToLoadBackwards = true ) assertThat(processedItems).isEmpty() @@ -146,8 +131,6 @@ class RoomBeginningPostProcessorTest { timelineItems, isDm = true, roomCreator = A_USER_ID, - joinRule = null, - isEncrypted = null, hasMoreToLoadBackwards = true ) assertThat(processedItems).isEmpty() @@ -164,80 +147,8 @@ class RoomBeginningPostProcessorTest { timelineItems, isDm = true, roomCreator = A_USER_ID, - joinRule = null, - isEncrypted = null, hasMoreToLoadBackwards = true ) assertThat(processedItems).isEqualTo(listOf(otherMemberJoinEvent)) } - - @Test - fun `processor removes join, leave, and profile events in unencrypted public rooms`() { - val timelineItems = listOf( - roomCreateEvent, - roomCreatorJoinEvent, - otherMemberJoinEvent, - messageEvent, - otherMemberLeaveEvent, - profileChangeEvent, - ) - val expected = listOf( - roomCreateEvent, - messageEvent, - ) - val processor = RoomBeginningPostProcessor(Timeline.Mode.Live) - val processedItems = processor.process( - timelineItems, - isDm = false, - roomCreator = A_USER_ID, - joinRule = JoinRule.Public, - isEncrypted = false, - hasMoreToLoadBackwards = false - ) - assertThat(processedItems).isEqualTo(expected) - } - - @Test - fun `processor keeps all events in encrypted public rooms`() { - val timelineItems = listOf( - roomCreateEvent, - roomCreatorJoinEvent, - otherMemberJoinEvent, - messageEvent, - otherMemberLeaveEvent, - profileChangeEvent, - ) - val processor = RoomBeginningPostProcessor(Timeline.Mode.Live) - val processedItems = processor.process( - timelineItems, - isDm = false, - roomCreator = A_USER_ID, - joinRule = JoinRule.Public, - isEncrypted = true, - hasMoreToLoadBackwards = false - ) - assertThat(processedItems).isEqualTo(timelineItems) - } - - @Test - fun `processor keeps membership events in invite-only rooms`() { - val timelineItems = listOf( - roomCreateEvent, - roomCreatorJoinEvent, - otherMemberJoinEvent, - messageEvent, - otherMemberLeaveEvent, - profileChangeEvent, - ) - val processor = RoomBeginningPostProcessor(Timeline.Mode.Live) - val processedItems = processor.process( - timelineItems, - isDm = false, - roomCreator = A_USER_ID, - joinRule = JoinRule.Invite, - isEncrypted = null, - hasMoreToLoadBackwards = false - ) - assertThat(processedItems).isEqualTo(timelineItems) - } }