Merge pull request #7539 from hayaksi1/fix/5356-sender-name-fallback

Name a sender the timeline has no profile for from the room member list
This commit is contained in:
Benoit Marty
2026-08-27 23:19:05 +02:00
committed by GitHub
2 changed files with 95 additions and 1 deletions
@@ -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<RoomMember>,
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<RoomMember>, 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())
@@ -20,8 +20,11 @@ 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.ThreadSummary
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_THREAD_ID
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.aRedactedContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
@@ -207,6 +210,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<TimelineItem.Event>().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<TimelineItem.Event>().single()
assertThat(event.senderProfile).isEqualTo(ProfileDetails.Unavailable)
cancelAndIgnoreRemainingEvents()
}
}
private suspend fun TestScope.groupPositionsOf(timestamps: List<Long>): List<TimelineItemGroupPosition> {
val factory = aTimelineItemsFactory(
config = TimelineItemsFactoryConfig(