Update timeline items read receipts when the room members are loaded (#2194)

* Update timeline items' sender info and read receipts when the room members info is loaded

* Only update this info if we have loaded the room members
This commit is contained in:
Jorge Martin Espinosa 2024-01-24 08:07:15 +01:00 committed by GitHub
parent 3faaf208b4
commit 14fc747e80
5 changed files with 97 additions and 23 deletions

1
changelog.d/2176.bugfix Normal file
View file

@ -0,0 +1 @@
Update timeline items' read receipts when the room members info is loaded.

View file

@ -53,6 +53,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu
import io.element.android.libraries.matrix.ui.room.canSendMessageAsState import io.element.android.libraries.matrix.ui.room.canSendMessageAsState
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -111,8 +112,6 @@ class TimelinePresenter @AssistedInject constructor(
} }
} }
val membersState by room.membersStateFlow.collectAsState()
fun handleEvents(event: TimelineEvents) { fun handleEvents(event: TimelineEvents) {
when (event) { when (event) {
TimelineEvents.LoadMore -> localScope.paginateBackwards() TimelineEvents.LoadMore -> localScope.paginateBackwards()
@ -149,13 +148,12 @@ class TimelinePresenter @AssistedInject constructor(
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
timeline combine(timeline.timelineItems, room.membersStateFlow) { items, membersState ->
.timelineItems
.onEach {
timelineItemsFactory.replaceWith( timelineItemsFactory.replaceWith(
timelineItems = it, timelineItems = items,
roomMembers = membersState.roomMembers().orEmpty() roomMembers = membersState.roomMembers().orEmpty()
) )
items
} }
.onEach { timelineItems -> .onEach { timelineItems ->
if (timelineItems.isEmpty()) { if (timelineItems.isEmpty()) {

View file

@ -87,7 +87,16 @@ class TimelineItemsFactory @Inject constructor(
newTimelineItemStates.add(timelineItemState) newTimelineItemStates.add(timelineItemState)
} }
} else { } else {
newTimelineItemStates.add(cacheItem) val updatedItem = if (cacheItem is TimelineItem.Event && roomMembers.isNotEmpty()) {
eventItemFactory.update(
timelineItem = cacheItem,
receivedMatrixTimelineItem = timelineItems[index] as MatrixTimelineItem.Event,
roomMembers = roomMembers
)
} else {
cacheItem
}
newTimelineItemStates.add(updatedItem)
} }
} }
val result = timelineItemGrouper.group(newTimelineItemStates).toPersistentList() val result = timelineItemGrouper.group(newTimelineItemStates).toPersistentList()

View file

@ -53,21 +53,7 @@ class TimelineItemEventFactory @Inject constructor(
val currentSender = currentTimelineItem.event.sender val currentSender = currentTimelineItem.event.sender
val groupPosition = val groupPosition =
computeGroupPosition(currentTimelineItem, timelineItems, index) computeGroupPosition(currentTimelineItem, timelineItems, index)
val senderDisplayName: String? val (senderDisplayName, senderAvatarUrl) = currentTimelineItem.getSenderInfo()
val senderAvatarUrl: String?
when (val senderProfile = currentTimelineItem.event.senderProfile) {
ProfileTimelineDetails.Unavailable,
ProfileTimelineDetails.Pending,
is ProfileTimelineDetails.Error -> {
senderDisplayName = null
senderAvatarUrl = null
}
is ProfileTimelineDetails.Ready -> {
senderDisplayName = senderProfile.getDisambiguatedDisplayName(currentSender)
senderAvatarUrl = senderProfile.avatarUrl
}
}
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT) val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
val sentTime = timeFormatter.format(Date(currentTimelineItem.event.timestamp)) val sentTime = timeFormatter.format(Date(currentTimelineItem.event.timestamp))
@ -101,6 +87,36 @@ class TimelineItemEventFactory @Inject constructor(
) )
} }
fun update(
timelineItem: TimelineItem.Event,
receivedMatrixTimelineItem: MatrixTimelineItem.Event,
roomMembers: List<RoomMember>,
): TimelineItem.Event {
return timelineItem.copy(
readReceiptState = receivedMatrixTimelineItem.computeReadReceiptState(roomMembers)
)
}
private fun MatrixTimelineItem.Event.getSenderInfo(): Pair<String?, String?> {
val senderDisplayName: String?
val senderAvatarUrl: String?
when (val senderProfile = event.senderProfile) {
ProfileTimelineDetails.Unavailable,
ProfileTimelineDetails.Pending,
is ProfileTimelineDetails.Error -> {
senderDisplayName = null
senderAvatarUrl = null
}
is ProfileTimelineDetails.Ready -> {
senderDisplayName = senderProfile.getDisambiguatedDisplayName(event.sender)
senderAvatarUrl = senderProfile.avatarUrl
}
}
return senderDisplayName to senderAvatarUrl
}
private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions { private fun MatrixTimelineItem.Event.computeReactionsState(): TimelineItemReactions {
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT) val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
var aggregatedReactions = event.reactions.map { reaction -> var aggregatedReactions = event.reactions.map { reaction ->

View file

@ -35,14 +35,18 @@ import io.element.android.features.poll.api.actions.SendPollResponseAction
import io.element.android.features.poll.test.actions.FakeEndPollAction import io.element.android.features.poll.test.actions.FakeEndPollAction
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.aMessageContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
@ -60,6 +64,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.util.Date import java.util.Date
import kotlin.time.Duration.Companion.seconds
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID" private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
@ -353,6 +358,50 @@ class TimelinePresenterTest {
} }
} }
@Test
fun `present - when room member info is loaded, read receipts info should be updated`() = runTest {
val timeline = FakeMatrixTimeline(
listOf(
MatrixTimelineItem.Event(
FAKE_UNIQUE_ID,
anEventTimelineItem(
sender = A_USER_ID,
receipts = persistentListOf(
Receipt(
userId = A_USER_ID,
timestamp = 0L,
)
)
)
)
)
)
val room = FakeMatrixRoom(matrixTimeline = timeline).apply {
givenRoomMembersState(MatrixRoomMembersState.Unknown)
}
val avatarUrl = "https://domain.com/avatar.jpg"
val presenter = createTimelinePresenter(timeline, room)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = consumeItemsUntilPredicate(30.seconds) { it.timelineItems.isNotEmpty() }.last()
val event = initialState.timelineItems.first() as TimelineItem.Event
assertThat(event.senderAvatar.url).isNull()
assertThat(event.readReceiptState.receipts.first().avatarData.url).isNull()
room.givenRoomMembersState(
MatrixRoomMembersState.Ready(
persistentListOf(aRoomMember(userId = A_USER_ID, avatarUrl = avatarUrl))
)
)
val updatedEvent = awaitItem().timelineItems.first() as TimelineItem.Event
assertThat(updatedEvent.readReceiptState.receipts.first().avatarData.url).isEqualTo(avatarUrl)
}
}
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T { private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
// Skip 1 item if Mentions feature is enabled // Skip 1 item if Mentions feature is enabled
if (FeatureFlags.Mentions.defaultValue) { if (FeatureFlags.Mentions.defaultValue) {
@ -363,6 +412,7 @@ class TimelinePresenterTest {
private fun TestScope.createTimelinePresenter( private fun TestScope.createTimelinePresenter(
timeline: MatrixTimeline = FakeMatrixTimeline(), timeline: MatrixTimeline = FakeMatrixTimeline(),
room: FakeMatrixRoom = FakeMatrixRoom(matrixTimeline = timeline),
timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory(), timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory(),
redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(), messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
@ -371,7 +421,7 @@ class TimelinePresenterTest {
): TimelinePresenter { ): TimelinePresenter {
return TimelinePresenter( return TimelinePresenter(
timelineItemsFactory = timelineItemsFactory, timelineItemsFactory = timelineItemsFactory,
room = FakeMatrixRoom(matrixTimeline = timeline), room = room,
dispatchers = testCoroutineDispatchers(), dispatchers = testCoroutineDispatchers(),
appScope = this, appScope = this,
navigator = messagesNavigator, navigator = messagesNavigator,