From e09d6e0a224da90cdedc99807f5248f34e64d7d6 Mon Sep 17 00:00:00 2001 From: hayaksi1 <193020925+hayaksi1@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:22:32 +0300 Subject: [PATCH] Name a sender the timeline has no profile for from the room member list The timeline renders a message with the sender's raw Matrix ID whenever the SDK has no profile for them, even though the room member list the same screen already holds usually knows their display name and avatar. The timeline item factory now falls back to the room member for a sender whose profile is not resolved, both when the item is first built and when the member list arrives afterwards, so the name and the avatar settle on the display name instead of staying on the ID. --- .../event/TimelineItemEventFactory.kt | 25 ++++++- .../factories/TimelineItemsFactoryTest.kt | 71 +++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index af3e18e3cc..6cd6b66cf1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt @@ -28,10 +28,12 @@ import io.element.android.libraries.dateformatter.api.DateFormatterMode import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo +import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName import io.element.android.libraries.matrix.ui.messages.reply.map @@ -64,7 +66,7 @@ class TimelineItemEventFactory( val currentSender = currentTimelineItem.event.sender val groupPosition = computeGroupPosition(currentTimelineItem, timelineItems, index) - val senderProfile = currentTimelineItem.event.senderProfile + val senderProfile = currentTimelineItem.event.senderProfile.orFallbackTo(roomMembers, currentSender) val sentTime = dateFormatter.format( timestamp = currentTimelineItem.event.timestamp, mode = DateFormatterMode.TimeOnly, @@ -138,11 +140,32 @@ class TimelineItemEventFactory( roomMembers: List, renderReadReceipts: Boolean, ): TimelineItem.Event { + val senderProfile = receivedMatrixTimelineItem.event.senderProfile.orFallbackTo(roomMembers, timelineItem.senderId) return timelineItem.copy( + senderProfile = senderProfile, + senderAvatar = timelineItem.senderAvatar.copy( + name = senderProfile.getDisambiguatedDisplayName(timelineItem.senderId), + url = senderProfile.getAvatarUrl(), + ), readReceiptState = receivedMatrixTimelineItem.computeReadReceiptState(roomMembers, renderReadReceipts) ) } + /** + * The timeline only knows the profiles the SDK has resolved, so a sender it has nothing for is rendered as a raw + * user ID. The room member list usually knows that user, so use it rather than showing the ID. + */ + private fun ProfileDetails.orFallbackTo(roomMembers: List, senderId: UserId): ProfileDetails { + if (this is ProfileDetails.Ready) return this + val member = roomMembers.find { it.userId == senderId } ?: return this + return ProfileDetails.Ready( + displayName = member.displayName, + displayNameAmbiguous = member.isNameAmbiguous, + avatarUrl = member.avatarUrl, + displayedStatus = member.displayedStatus, + ) + } + private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions { if (!config.computeReactions) { return TimelineItemReactions(reactions = persistentListOf()) 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 index f93c8f196e..3c51fc88ef 100644 --- 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 @@ -16,7 +16,10 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt 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.event.OtherMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.ProfileDetails 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.room.aRoomMember import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import kotlinx.coroutines.test.TestScope @@ -128,6 +131,74 @@ class TimelineItemsFactoryTest { } } + @Test + fun `a sender the timeline knows nothing about is named from the room member list`() = runTest { + val factory = aTimelineItemsFactory( + config = TimelineItemsFactoryConfig( + computeReadReceipts = false, + computeReactions = false, + ) + ) + val items = listOf( + MatrixTimelineItem.Event( + uniqueId = UniqueId("event-0"), + event = anEventTimelineItem( + sender = A_USER_ID, + senderProfile = ProfileDetails.Unavailable, + content = aMessageContent(body = "A regular message"), + ), + ), + ) + factory.timelineItems.test { + factory.replaceWith( + timelineItems = items, + roomMembers = listOf(aRoomMember(userId = A_USER_ID, displayName = "Alice")), + renderReadReceipts = false, + ) + val event = awaitItem().filterIsInstance().single() + assertThat(event.senderProfile).isEqualTo( + ProfileDetails.Ready( + displayName = "Alice", + displayNameAmbiguous = false, + avatarUrl = null, + displayedStatus = null, + ) + ) + assertThat(event.senderAvatar.name).isEqualTo("Alice") + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `a sender the room member list knows nothing about keeps the profile the timeline gave`() = runTest { + val factory = aTimelineItemsFactory( + config = TimelineItemsFactoryConfig( + computeReadReceipts = false, + computeReactions = false, + ) + ) + val items = listOf( + MatrixTimelineItem.Event( + uniqueId = UniqueId("event-0"), + event = anEventTimelineItem( + sender = A_USER_ID, + senderProfile = ProfileDetails.Unavailable, + content = aMessageContent(body = "A regular message"), + ), + ), + ) + factory.timelineItems.test { + factory.replaceWith( + timelineItems = items, + roomMembers = listOf(aRoomMember(userId = A_USER_ID_2, displayName = "Bob")), + renderReadReceipts = false, + ) + val event = awaitItem().filterIsInstance().single() + assertThat(event.senderProfile).isEqualTo(ProfileDetails.Unavailable) + cancelAndIgnoreRemainingEvents() + } + } + private suspend fun TestScope.groupPositionsOf(timestamps: List): List { val factory = aTimelineItemsFactory( config = TimelineItemsFactoryConfig(