Repository: element-hq/element-x-android (language: kotlin)

Issue:
Remove verbose state events from the timeline
When creating a room, we can see at least 7 state events being added to the timeline:

- Who created the room (`m.room.create`).
- The same person joining the room (`m.room.member` with membership `join`).
- The room power levels being set (`m.room.power_levels`).
- The room join rules being set up (`m.room.join_rules`).
- The room history visibility being set up (`m.room.history_visibility`).
- The room guest access being set up (`m.room.guest_access`).
- If encryption is enabled or disabled (`m.room.encryption`).

Plus any invites.

From those, we only want to display:

- Who created the room.
- Who joined the room.
- If encryption is enabled.
- The invites.

So we need to filter out the rest of the events from the timeline. Following iOS' implementation, this removal should be done globally in the timeline, not just for the few first items.


Discussion hints:
nan

Pull request: Hide verbose state events from the timeline
<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
 
## Type of change

- [ ] Feature
- [x] Bugfix
- [ ] Technical
- [ ] Other :

## Content

Added a filter for the timeline items that removes 'hidden' or 'not visible' state events from the timeline.

## Motivation and context

Fixes #2216.

## Screenshots / GIFs

No changes in screenshots.

## Tests

- Create a new room or open a recently created one.

Instead of 7-8 state events you should only see 3-4 (created & joined room, room encrypted, invited another user).

## Tested devices

- [x] Physical
- [ ] Emulator
- OS version(s): 14

## Checklist

<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->

- [ ] Changes have been tested on an Android device or Android emulator with API 23
- [ ] UI change has been tested on both light and dark themes
- [ ] Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [x] Pull request is based on the develop branch
- [x] Pull request includes a new file under ./changelog.d. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#changelog
- [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off)
- [x] You've made a self review of your PR


Test command: ./gradlew :libraries:matrix:impl:testDebugUnitTest --tests "io.element.android.libraries.matrix.impl.timeline.postprocessor.FilterHiddenStateEventsProcessorTest"

Fix patch:
diff --git a/changelog.d/2216.bugfix b/changelog.d/2216.bugfix
new file mode 100644
index 00000000000..a47f2976b54
--- /dev/null
+++ b/changelog.d/2216.bugfix
@@ -0,0 +1,1 @@
+Hide verbose state events from the timeline
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt
index fc4840d6105..87e4f4ab73d 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt
@@ -85,7 +85,11 @@ data class ProfileChangeContent(
 data class StateContent(
     val stateKey: String,
     val content: OtherState
-) : EventContent
+) : EventContent {
+    fun isVisibleInTimeline(): Boolean {
+        return content.isVisibleInTimeline()
+    }
+}
 
 data class FailedToParseMessageLikeContent(
     val eventType: String,
diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt
index 6960b3565d2..e5119c388e0 100644
--- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt
+++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/OtherState.kt
@@ -41,4 +41,30 @@ sealed interface OtherState {
     data object SpaceChild : OtherState
     data object SpaceParent : OtherState
     data class Custom(val eventType: String) : OtherState
+
+    fun isVisibleInTimeline() = when (this) {
+        // Visible
+        is RoomAvatar,
+        is RoomName,
+        is RoomTopic,
+        is RoomThirdPartyInvite,
+        is RoomCreate,
+        is RoomEncryption,
+        is Custom -> true
+        // Hidden
+        is RoomAliases,
+        is RoomCanonicalAlias,
+        is RoomGuestAccess,
+        is RoomHistoryVisibility,
+        is RoomJoinRules,
+        is RoomPinnedEvents,
+        is RoomPowerLevels,
+        is RoomServerAcl,
+        is RoomTombstone,
+        is SpaceChild,
+        is SpaceParent,
+        is PolicyRuleRoom,
+        is PolicyRuleServer,
+        is PolicyRuleUser -> false
+    }
 }
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt
index 80c8f1d391a..1841aa4b595 100644
--- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt
@@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessage
 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.FilterHiddenStateEventsProcessor
 import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineDispatcher
@@ -82,6 +83,8 @@ class RustMatrixTimeline(
         dispatcher = dispatcher,
     )
 
+    private val filterHiddenStateEventsProcessor = FilterHiddenStateEventsProcessor()
+
     private val timelineItemFactory = MatrixTimelineItemMapper(
         fetchDetailsForEvent = this::fetchDetailsForEvent,
         roomCoroutineScope = roomCoroutineScope,
@@ -101,9 +104,9 @@ class RustMatrixTimeline(
     override val paginationState: StateFlow<MatrixTimeline.PaginationState> = _paginationState.asStateFlow()
 
     @OptIn(ExperimentalCoroutinesApi::class)
-    override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems.mapLatest { items ->
-        encryptedHistoryPostProcessor.process(items)
-    }
+    override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems
+        .mapLatest { items -> encryptedHistoryPostProcessor.process(items) }
+        .mapLatest { items -> filterHiddenStateEventsProcessor.process(items) }
 
     init {
         Timber.d("Initialize timeline for room ${matrixRoom.roomId}")
diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterHiddenStateEventsProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterHiddenStateEventsProcessor.kt
new file mode 100644
index 00000000000..c54221d84eb
--- /dev/null
+++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterHiddenStateEventsProcessor.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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.event.StateContent
+
+/**
+ * This class is used to filter out 'hidden' state events from the timeline.
+ */
+class FilterHiddenStateEventsProcessor {
+    fun process(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> {
+        return items.filter { item ->
+            when (item) {
+                is MatrixTimelineItem.Event -> {
+                    when (val content = item.event.content) {
+                        // If it's a state event, make sure it's visible
+                        is StateContent -> content.isVisibleInTimeline()
+                        // We can display any other event
+                        else -> true
+                    }
+                }
+                is MatrixTimelineItem.Virtual -> true
+                is MatrixTimelineItem.Other -> true
+            }
+        }
+    }
+}


Test patch:
diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterHiddenStateEventsProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterHiddenStateEventsProcessorTest.kt
new file mode 100644
index 00000000000..0532d1b7624
--- /dev/null
+++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/FilterHiddenStateEventsProcessorTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2024 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.element.android.libraries.matrix.impl.timeline.postprocessor
+
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
+import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
+import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
+import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
+import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
+import org.junit.Test
+
+class FilterHiddenStateEventsProcessorTest {
+    @Test
+    fun test() {
+        val items = listOf(
+            // These are visible because they're not state events
+            MatrixTimelineItem.Other,
+            MatrixTimelineItem.Virtual("virtual", VirtualTimelineItem.ReadMarker),
+            MatrixTimelineItem.Event("event", anEventTimelineItem()),
+            // These are visible state events
+            MatrixTimelineItem.Event("m.room.avatar", anEventTimelineItem(content = StateContent("", OtherState.RoomAvatar("")))),
+            MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(content = StateContent("", OtherState.RoomCreate))),
+            MatrixTimelineItem.Event("m.room.encrypted", anEventTimelineItem(content = StateContent("", OtherState.RoomEncryption))),
+            MatrixTimelineItem.Event("m.room.name", anEventTimelineItem(content = StateContent("", OtherState.RoomName("")))),
+            MatrixTimelineItem.Event("m.room.third_party_invite", anEventTimelineItem(content = StateContent("", OtherState.RoomThirdPartyInvite("")))),
+            MatrixTimelineItem.Event("m.room.topic", anEventTimelineItem(content = StateContent("", OtherState.RoomTopic("")))),
+            MatrixTimelineItem.Event("m.room.custom", anEventTimelineItem(content = StateContent("", OtherState.Custom("")))),
+            // These ones are hidden
+            MatrixTimelineItem.Event("m.room.aliases", anEventTimelineItem(content = StateContent("", OtherState.RoomAliases))),
+            MatrixTimelineItem.Event("m.room.canonical_alias", anEventTimelineItem(content = StateContent("", OtherState.RoomCanonicalAlias))),
+            MatrixTimelineItem.Event("m.room.guest_access", anEventTimelineItem(content = StateContent("", OtherState.RoomGuestAccess))),
+            MatrixTimelineItem.Event("m.room.history_visibility", anEventTimelineItem(content = StateContent("", OtherState.RoomHistoryVisibility))),
+            MatrixTimelineItem.Event("m.room.join_rules", anEventTimelineItem(content = StateContent("", OtherState.RoomJoinRules))),
+            MatrixTimelineItem.Event("m.room.pinned_events", anEventTimelineItem(content = StateContent("", OtherState.RoomPinnedEvents))),
+            MatrixTimelineItem.Event("m.room.power_levels", anEventTimelineItem(content = StateContent("", OtherState.RoomPowerLevels))),
+            MatrixTimelineItem.Event("m.room.server_acl", anEventTimelineItem(content = StateContent("", OtherState.RoomServerAcl))),
+            MatrixTimelineItem.Event("m.room.tombstone", anEventTimelineItem(content = StateContent("", OtherState.RoomTombstone))),
+            MatrixTimelineItem.Event("m.space.child", anEventTimelineItem(content = StateContent("", OtherState.SpaceChild))),
+            MatrixTimelineItem.Event("m.space.parent", anEventTimelineItem(content = StateContent("", OtherState.SpaceParent))),
+            MatrixTimelineItem.Event("m.room.policy.rule.room", anEventTimelineItem(content = StateContent("", OtherState.PolicyRuleRoom))),
+            MatrixTimelineItem.Event("m.room.policy.rule.server", anEventTimelineItem(content = StateContent("", OtherState.PolicyRuleServer))),
+            MatrixTimelineItem.Event("m.room.policy.rule.user", anEventTimelineItem(content = StateContent("", OtherState.PolicyRuleUser))),
+        )
+
+        val expected = listOf(
+            MatrixTimelineItem.Other,
+            MatrixTimelineItem.Virtual("virtual", VirtualTimelineItem.ReadMarker),
+            MatrixTimelineItem.Event("event", anEventTimelineItem()),
+            MatrixTimelineItem.Event("m.room.avatar", anEventTimelineItem(content = StateContent("", OtherState.RoomAvatar("")))),
+            MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(content = StateContent("", OtherState.RoomCreate))),
+            MatrixTimelineItem.Event("m.room.encrypted", anEventTimelineItem(content = StateContent("", OtherState.RoomEncryption))),
+            MatrixTimelineItem.Event("m.room.name", anEventTimelineItem(content = StateContent("", OtherState.RoomName("")))),
+            MatrixTimelineItem.Event("m.room.third_party_invite", anEventTimelineItem(content = StateContent("", OtherState.RoomThirdPartyInvite("")))),
+            MatrixTimelineItem.Event("m.room.topic", anEventTimelineItem(content = StateContent("", OtherState.RoomTopic("")))),
+            MatrixTimelineItem.Event("m.room.custom", anEventTimelineItem(content = StateContent("", OtherState.Custom("")))),
+        )
+
+        val processor = FilterHiddenStateEventsProcessor()
+
+        assertThat(processor.process(items)).isEqualTo(expected)
+    }
+}