Do not show membership/profile events in public rooms (#6360)

* Filter some membership/profile/topic events in public rooms: don't display join and leave membership events in publicly joinable rooms, and hide display name and avatar url changes in non encrypted and publicly joinable rooms.

* Add empty day post-processing to the timeline based on bxdxnn's code, tweaked.

---------

Co-authored-by: Jorge Martín <jorgem@element.io>
This commit is contained in:
bxdxnn 2026-05-25 11:31:53 +03:00 committed by GitHub
parent 5277382e6d
commit b31dad4b26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 339 additions and 8 deletions

View file

@ -12,6 +12,7 @@ import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
@ -20,6 +21,7 @@ import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.IntentionalMention
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.MsgType
@ -43,6 +45,7 @@ import io.element.android.libraries.matrix.impl.timeline.postprocessor.TypingNot
import io.element.android.libraries.matrix.impl.timeline.reply.InReplyToMapper
import io.element.android.libraries.matrix.impl.util.MessageEventContent
import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -121,6 +124,13 @@ class RustTimeline(
private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(mode)
private val typingNotificationPostProcessor = TypingNotificationPostProcessor(mode)
private data class RoomTimelineInfo(
val roomCreators: ImmutableList<UserId>,
val isDm: Boolean,
val joinRule: JoinRule?,
val isEncrypted: Boolean?,
)
override val backwardPaginationStatus = MutableStateFlow(
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode != Timeline.Mode.PinnedEvents)
)
@ -220,20 +230,23 @@ class RustTimeline(
_timelineItems,
backwardPaginationStatus,
forwardPaginationStatus,
joinedRoom.roomInfoFlow.map { it.creators to it.isDm }.distinctUntilChanged(),
joinedRoom.roomInfoFlow.map { RoomTimelineInfo(it.creators, it.isDm, it.joinRule, it.isEncrypted) }.distinctUntilChanged(),
) {
timelineItems,
backwardPaginationStatus,
forwardPaginationStatus,
(roomCreators, isDm),
roomInfo,
->
withContext(dispatcher) {
val (roomCreators, isDm, joinRule, isEncrypted) = roomInfo
timelineItems
.let { items ->
roomBeginningPostProcessor.process(
items = items,
isDm = isDm,
roomCreator = roomCreators.firstOrNull(),
joinRule = joinRule,
isEncrypted = isEncrypted,
hasMoreToLoadBackwards = backwardPaginationStatus.hasMoreToLoad,
)
}

View file

@ -9,33 +9,49 @@
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
/**
* This timeline post-processor removes the room creation event and the self-join event from the timeline for DMs
* or add the RoomBeginning item.
* or add the RoomBeginning item. For rooms that aren't invite-only and aren't encrypted, it also removes join/leave and profile change events.
*/
class RoomBeginningPostProcessor(private val mode: Timeline.Mode) {
fun process(
items: List<MatrixTimelineItem>,
isDm: Boolean,
roomCreator: UserId?,
joinRule: JoinRule?,
isEncrypted: Boolean?,
hasMoreToLoadBackwards: Boolean,
): List<MatrixTimelineItem> {
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<MatrixTimelineItem>): List<MatrixTimelineItem> {
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<MatrixTimelineItem>): List<MatrixTimelineItem> {
// No changes needed, timeline start item is already added by the SDK
return items

View file

@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTime
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.aProfileChangeMessageContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import io.element.android.libraries.matrix.test.timeline.item.event.aRoomMembershipContent
@ -36,6 +37,14 @@ internal val otherMemberJoinEvent = MatrixTimelineItem.Event(
uniqueId = UniqueId("m.room.member_other"),
event = anEventTimelineItem(content = aRoomMembershipContent(userId = A_USER_ID_2, change = MembershipChange.JOINED))
)
internal val otherMemberLeaveEvent = MatrixTimelineItem.Event(
uniqueId = UniqueId("m.room.member_leave"),
event = anEventTimelineItem(content = aRoomMembershipContent(userId = A_USER_ID_2, change = MembershipChange.LEFT))
)
internal val profileChangeEvent = MatrixTimelineItem.Event(
uniqueId = UniqueId("m.room.member_profile"),
event = anEventTimelineItem(content = aProfileChangeMessageContent(displayName = "New Name", prevDisplayName = "Old Name"))
)
internal val messageEvent = MatrixTimelineItem.Event(
uniqueId = UniqueId("m.room.message"),
event = anEventTimelineItem(content = aMessageContent("hi"))

View file

@ -9,6 +9,7 @@
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
@ -21,6 +22,8 @@ class RoomBeginningPostProcessorTest {
items = emptyList(),
isDm = true,
roomCreator = A_USER_ID,
joinRule = null,
isEncrypted = null,
hasMoreToLoadBackwards = false,
)
assertThat(processedItems).isEmpty()
@ -33,6 +36,8 @@ class RoomBeginningPostProcessorTest {
items = listOf(messageEvent),
isDm = true,
roomCreator = A_USER_ID,
joinRule = null,
isEncrypted = null,
hasMoreToLoadBackwards = false,
)
assertThat(processedItems).isEqualTo(listOf(messageEvent))
@ -45,6 +50,8 @@ class RoomBeginningPostProcessorTest {
items = listOf(messageEvent),
isDm = true,
roomCreator = null,
joinRule = null,
isEncrypted = null,
hasMoreToLoadBackwards = false,
)
assertThat(processedItems).isEqualTo(listOf(messageEvent))
@ -62,6 +69,8 @@ class RoomBeginningPostProcessorTest {
items = timelineItems,
isDm = true,
roomCreator = A_USER_ID,
joinRule = null,
isEncrypted = null,
hasMoreToLoadBackwards = false,
)
assertThat(processedItems).containsExactly(timelineStartEvent)
@ -78,6 +87,8 @@ class RoomBeginningPostProcessorTest {
items = timelineItems,
isDm = true,
roomCreator = A_USER_ID,
joinRule = null,
isEncrypted = null,
hasMoreToLoadBackwards = false,
)
assertThat(processedItems).isEqualTo(timelineItems)
@ -96,7 +107,14 @@ class RoomBeginningPostProcessorTest {
messageEvent,
)
val processor = RoomBeginningPostProcessor(Timeline.Mode.Live)
val processedItems = processor.process(timelineItems, isDm = true, roomCreator = A_USER_ID, hasMoreToLoadBackwards = false)
val processedItems = processor.process(
timelineItems,
isDm = true,
roomCreator = A_USER_ID,
joinRule = null,
isEncrypted = null,
hasMoreToLoadBackwards = false
)
assertThat(processedItems).isEqualTo(expected)
}
@ -107,7 +125,14 @@ class RoomBeginningPostProcessorTest {
roomCreatorJoinEvent,
)
val processor = RoomBeginningPostProcessor(Timeline.Mode.Live)
val processedItems = processor.process(timelineItems, isDm = true, roomCreator = A_USER_ID, hasMoreToLoadBackwards = true)
val processedItems = processor.process(
timelineItems,
isDm = true,
roomCreator = A_USER_ID,
joinRule = null,
isEncrypted = null,
hasMoreToLoadBackwards = true
)
assertThat(processedItems).isEmpty()
}
@ -117,7 +142,14 @@ class RoomBeginningPostProcessorTest {
roomCreatorJoinEvent,
)
val processor = RoomBeginningPostProcessor(Timeline.Mode.Live)
val processedItems = processor.process(timelineItems, isDm = true, roomCreator = A_USER_ID, hasMoreToLoadBackwards = true)
val processedItems = processor.process(
timelineItems,
isDm = true,
roomCreator = A_USER_ID,
joinRule = null,
isEncrypted = null,
hasMoreToLoadBackwards = true
)
assertThat(processedItems).isEmpty()
}
@ -128,7 +160,84 @@ class RoomBeginningPostProcessorTest {
otherMemberJoinEvent,
)
val processor = RoomBeginningPostProcessor(Timeline.Mode.Live)
val processedItems = processor.process(timelineItems, isDm = true, roomCreator = A_USER_ID, hasMoreToLoadBackwards = true)
val processedItems = processor.process(
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)
}
}