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:
parent
3faaf208b4
commit
14fc747e80
5 changed files with 97 additions and 23 deletions
1
changelog.d/2176.bugfix
Normal file
1
changelog.d/2176.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Update timeline items' read receipts when the room members info is loaded.
|
||||||
|
|
@ -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()) {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 ->
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue