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(