diff --git a/changelog.d/2217.misc b/changelog.d/2217.misc new file mode 100644 index 0000000000..40d4bd7fc6 --- /dev/null +++ b/changelog.d/2217.misc @@ -0,0 +1 @@ +Remove room creation, self-join of room creator and 'this is the beginning of X' timeline items for DMs. diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 4f686e594b..d2ab635e6a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -145,7 +145,7 @@ fun TimelineView( } } } - if (state.paginationState.beginningOfRoomReached) { + if (state.paginationState.beginningOfRoomReached && !state.timelineRoomInfo.isDirect) { item(contentType = "BeginningOfRoomReached") { TimelineItemRoomBeginningView(roomName = roomName) } 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 f33e0606e7..7b1e7d0500 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.DmBeginningTimelineProcessor import io.element.android.libraries.matrix.impl.timeline.postprocessor.FilterHiddenStateEventsProcessor import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor import kotlinx.coroutines.CompletableDeferred @@ -85,6 +86,8 @@ class RustMatrixTimeline( private val filterHiddenStateEventsProcessor = FilterHiddenStateEventsProcessor() + private val dmBeginningTimelineProcessor = DmBeginningTimelineProcessor() + private val timelineItemFactory = MatrixTimelineItemMapper( fetchDetailsForEvent = this::fetchDetailsForEvent, roomCoroutineScope = roomCoroutineScope, @@ -107,6 +110,13 @@ class RustMatrixTimeline( override val timelineItems: Flow> = _timelineItems .mapLatest { items -> encryptedHistoryPostProcessor.process(items) } .mapLatest { items -> filterHiddenStateEventsProcessor.process(items) } + .mapLatest { items -> + dmBeginningTimelineProcessor.process( + items = items, + isDm = matrixRoom.isDirect && matrixRoom.isOneToOne, + isAtStartOfTimeline = paginationState.value.beginningOfRoomReached + ) + } 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/DmBeginningTimelineProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/DmBeginningTimelineProcessor.kt new file mode 100644 index 0000000000..f36b4a78b8 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/DmBeginningTimelineProcessor.kt @@ -0,0 +1,61 @@ +/* + * 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.MembershipChange +import io.element.android.libraries.matrix.api.timeline.item.event.OtherState +import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent +import io.element.android.libraries.matrix.api.timeline.item.event.StateContent + +/** + * This timeline post-processor removes the room creation event and the self-join event from the timeline for DMs. + */ +class DmBeginningTimelineProcessor { + fun process( + items: List, + isDm: Boolean, + isAtStartOfTimeline: Boolean + ): List { + if (!isDm || !isAtStartOfTimeline) return items + + // Find room creation event. This is usually index 0 + val roomCreationEventIndex = items.indexOfFirst { + val stateEventContent = (it as? MatrixTimelineItem.Event)?.event?.content as? StateContent + stateEventContent?.content is OtherState.RoomCreate + } + + // Find self-join event for room creator. This is usually index 1 + val roomCreatorUserId = (items.getOrNull(roomCreationEventIndex) as? MatrixTimelineItem.Event)?.event?.sender + val selfUserJoinedEventIndex = roomCreatorUserId?.let { creatorUserId -> + items.indexOfFirst { + val stateEventContent = (it as? MatrixTimelineItem.Event)?.event?.content as? RoomMembershipContent + stateEventContent?.change == MembershipChange.JOINED && stateEventContent.userId == creatorUserId + } + } ?: -1 + + // Remove items at the indices we found + val newItems = items.toMutableList() + if (selfUserJoinedEventIndex in newItems.indices) { + newItems.removeAt(selfUserJoinedEventIndex) + } + if (roomCreationEventIndex in newItems.indices) { + newItems.removeAt(roomCreationEventIndex) + } + return newItems + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/DmBeginningTimelineProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/DmBeginningTimelineProcessorTest.kt new file mode 100644 index 0000000000..fc24f54c14 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/DmBeginningTimelineProcessorTest.kt @@ -0,0 +1,102 @@ +/* + * 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.MembershipChange +import io.element.android.libraries.matrix.api.timeline.item.event.OtherState +import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent +import io.element.android.libraries.matrix.api.timeline.item.event.StateContent +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.timeline.aMessageContent +import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem +import org.junit.Test + +class DmBeginningTimelineProcessorTest { + @Test + fun `processor removes room creation event and self-join event from DM timeline`() { + val timelineItems = listOf( + MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), + MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))), + ) + val processor = DmBeginningTimelineProcessor() + val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = true) + assertThat(processedItems).isEmpty() + } + + @Test + fun `processor removes room creation event and self-join event from DM timeline even if they're not the first items`() { + val timelineItems = listOf( + MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))), + MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), + MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))), + MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))), + ) + val expected = listOf( + MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))), + MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))), + ) + val processor = DmBeginningTimelineProcessor() + val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = true) + assertThat(processedItems).isEqualTo(expected) + } + + @Test + fun `processor won't remove items if it's not a DM`() { + val timelineItems = listOf( + MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), + MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))), + ) + val processor = DmBeginningTimelineProcessor() + val processedItems = processor.process(timelineItems, isDm = false, isAtStartOfTimeline = true) + assertThat(processedItems).isEqualTo(timelineItems) + } + + @Test + fun `processor won't remove items if it's not at the start of the timeline`() { + val timelineItems = listOf( + MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), + MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))), + ) + val processor = DmBeginningTimelineProcessor() + val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = false) + assertThat(processedItems).isEqualTo(timelineItems) + } + + @Test + fun `processor won't remove the first member join event if it can't find the room creation event`() { + val timelineItems = listOf( + MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))), + ) + val processor = DmBeginningTimelineProcessor() + val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = false) + assertThat(processedItems).isEqualTo(timelineItems) + } + + @Test + fun `processor won't remove the first member join event if it's not from the room creator`() { + val timelineItems = listOf( + MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), + MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))), + ) + val processor = DmBeginningTimelineProcessor() + val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = false) + assertThat(processedItems).isEqualTo(timelineItems) + } +}