diff --git a/changelog.d/2216.bugfix b/changelog.d/2216.bugfix new file mode 100644 index 0000000000..a47f2976b5 --- /dev/null +++ b/changelog.d/2216.bugfix @@ -0,0 +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 fc4840d610..87e4f4ab73 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 6960b3565d..e5119c388e 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 80c8f1d391..1841aa4b59 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 = _paginationState.asStateFlow() @OptIn(ExperimentalCoroutinesApi::class) - override val timelineItems: Flow> = _timelineItems.mapLatest { items -> - encryptedHistoryPostProcessor.process(items) - } + override val timelineItems: Flow> = _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 0000000000..c54221d84e --- /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): List { + 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 + } + } + } +} 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 0000000000..0532d1b762 --- /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) + } +}