Merge pull request #2041 from element-hq/feature/fga/fix_timeline_back_pagination_loop
Fix timeline back pagination loop in encrypted room.
This commit is contained in:
commit
6b16995812
10 changed files with 84 additions and 123 deletions
|
|
@ -80,7 +80,7 @@ class TimelineItemEventFactory @Inject constructor(
|
||||||
)
|
)
|
||||||
currentTimelineItem.event
|
currentTimelineItem.event
|
||||||
return TimelineItem.Event(
|
return TimelineItem.Event(
|
||||||
id = currentTimelineItem.uniqueId.toString(),
|
id = currentTimelineItem.uniqueId,
|
||||||
eventId = currentTimelineItem.eventId,
|
eventId = currentTimelineItem.eventId,
|
||||||
transactionId = currentTimelineItem.transactionId,
|
transactionId = currentTimelineItem.transactionId,
|
||||||
senderId = currentSender,
|
senderId = currentSender,
|
||||||
|
|
|
||||||
|
|
@ -31,13 +31,8 @@ class TimelineItemVirtualFactory @Inject constructor(
|
||||||
fun create(
|
fun create(
|
||||||
virtualTimelineItem: MatrixTimelineItem.Virtual,
|
virtualTimelineItem: MatrixTimelineItem.Virtual,
|
||||||
): TimelineItem.Virtual {
|
): TimelineItem.Virtual {
|
||||||
val id = if (virtualTimelineItem.virtual is VirtualTimelineItem.EncryptedHistoryBanner) {
|
|
||||||
"encrypted_history_banner"
|
|
||||||
} else {
|
|
||||||
virtualTimelineItem.uniqueId.toString()
|
|
||||||
}
|
|
||||||
return TimelineItem.Virtual(
|
return TimelineItem.Virtual(
|
||||||
id = id,
|
id = virtualTimelineItem.uniqueId,
|
||||||
model = virtualTimelineItem.computeModel()
|
model = virtualTimelineItem.computeModel()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@ import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
|
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
|
||||||
|
|
||||||
class TimelinePresenterTest {
|
class TimelinePresenterTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
|
|
@ -120,7 +122,7 @@ class TimelinePresenterTest {
|
||||||
fun `present - on scroll finished send read receipt if an event is before the index`() = runTest {
|
fun `present - on scroll finished send read receipt if an event is before the index`() = runTest {
|
||||||
val timeline = FakeMatrixTimeline(
|
val timeline = FakeMatrixTimeline(
|
||||||
initialTimelineItems = listOf(
|
initialTimelineItems = listOf(
|
||||||
MatrixTimelineItem.Event(0, anEventTimelineItem())
|
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val presenter = createTimelinePresenter(timeline)
|
val presenter = createTimelinePresenter(timeline)
|
||||||
|
|
@ -144,7 +146,7 @@ class TimelinePresenterTest {
|
||||||
fun `present - on scroll finished will not send read receipt if no event is before the index`() = runTest {
|
fun `present - on scroll finished will not send read receipt if no event is before the index`() = runTest {
|
||||||
val timeline = FakeMatrixTimeline(
|
val timeline = FakeMatrixTimeline(
|
||||||
initialTimelineItems = listOf(
|
initialTimelineItems = listOf(
|
||||||
MatrixTimelineItem.Event(0, anEventTimelineItem())
|
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val presenter = createTimelinePresenter(timeline)
|
val presenter = createTimelinePresenter(timeline)
|
||||||
|
|
@ -168,7 +170,7 @@ class TimelinePresenterTest {
|
||||||
fun `present - on scroll finished will not send read receipt only virtual events exist before the index`() = runTest {
|
fun `present - on scroll finished will not send read receipt only virtual events exist before the index`() = runTest {
|
||||||
val timeline = FakeMatrixTimeline(
|
val timeline = FakeMatrixTimeline(
|
||||||
initialTimelineItems = listOf(
|
initialTimelineItems = listOf(
|
||||||
MatrixTimelineItem.Virtual(0, VirtualTimelineItem.ReadMarker)
|
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val presenter = createTimelinePresenter(timeline)
|
val presenter = createTimelinePresenter(timeline)
|
||||||
|
|
@ -199,13 +201,13 @@ class TimelinePresenterTest {
|
||||||
assertThat(initialState.newEventState).isEqualTo(NewEventState.None)
|
assertThat(initialState.newEventState).isEqualTo(NewEventState.None)
|
||||||
assertThat(initialState.timelineItems.size).isEqualTo(0)
|
assertThat(initialState.timelineItems.size).isEqualTo(0)
|
||||||
timeline.updateTimelineItems {
|
timeline.updateTimelineItems {
|
||||||
listOf(MatrixTimelineItem.Event(0, anEventTimelineItem(content = aMessageContent())))
|
listOf(MatrixTimelineItem.Event("0", anEventTimelineItem(content = aMessageContent())))
|
||||||
}
|
}
|
||||||
consumeItemsUntilPredicate { it.timelineItems.size == 1 }
|
consumeItemsUntilPredicate { it.timelineItems.size == 1 }
|
||||||
// Mimics sending a message, and assert newEventState is FromMe
|
// Mimics sending a message, and assert newEventState is FromMe
|
||||||
timeline.updateTimelineItems { items ->
|
timeline.updateTimelineItems { items ->
|
||||||
val event = anEventTimelineItem(content = aMessageContent(), isOwn = true)
|
val event = anEventTimelineItem(content = aMessageContent(), isOwn = true)
|
||||||
items + listOf(MatrixTimelineItem.Event(1, event))
|
items + listOf(MatrixTimelineItem.Event("1", event))
|
||||||
}
|
}
|
||||||
consumeItemsUntilPredicate { it.timelineItems.size == 2 }
|
consumeItemsUntilPredicate { it.timelineItems.size == 2 }
|
||||||
awaitLastSequentialItem().also { state ->
|
awaitLastSequentialItem().also { state ->
|
||||||
|
|
@ -214,7 +216,7 @@ class TimelinePresenterTest {
|
||||||
// Mimics receiving a message without clearing the previous FromMe
|
// Mimics receiving a message without clearing the previous FromMe
|
||||||
timeline.updateTimelineItems { items ->
|
timeline.updateTimelineItems { items ->
|
||||||
val event = anEventTimelineItem(content = aMessageContent())
|
val event = anEventTimelineItem(content = aMessageContent())
|
||||||
items + listOf(MatrixTimelineItem.Event(2, event))
|
items + listOf(MatrixTimelineItem.Event("2", event))
|
||||||
}
|
}
|
||||||
consumeItemsUntilPredicate { it.timelineItems.size == 3 }
|
consumeItemsUntilPredicate { it.timelineItems.size == 3 }
|
||||||
|
|
||||||
|
|
@ -226,7 +228,7 @@ class TimelinePresenterTest {
|
||||||
// Mimics receiving a message and assert newEventState is FromOther
|
// Mimics receiving a message and assert newEventState is FromOther
|
||||||
timeline.updateTimelineItems { items ->
|
timeline.updateTimelineItems { items ->
|
||||||
val event = anEventTimelineItem(content = aMessageContent())
|
val event = anEventTimelineItem(content = aMessageContent())
|
||||||
items + listOf(MatrixTimelineItem.Event(3, event))
|
items + listOf(MatrixTimelineItem.Event("3", event))
|
||||||
}
|
}
|
||||||
consumeItemsUntilPredicate { it.timelineItems.size == 4 }
|
consumeItemsUntilPredicate { it.timelineItems.size == 4 }
|
||||||
awaitLastSequentialItem().also { state ->
|
awaitLastSequentialItem().also { state ->
|
||||||
|
|
@ -267,7 +269,7 @@ class TimelinePresenterTest {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
timeline.updateTimelineItems {
|
timeline.updateTimelineItems {
|
||||||
listOf(MatrixTimelineItem.Event(0, anEventTimelineItem(reactions = oneReaction)))
|
listOf(MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(reactions = oneReaction)))
|
||||||
}
|
}
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val item = awaitItem().timelineItems.first()
|
val item = awaitItem().timelineItems.first()
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ fun TestScope.aDefaultRedactedVoiceMessageManager(
|
||||||
|
|
||||||
fun aRedactedMatrixTimeline(eventId: EventId) = listOf<MatrixTimelineItem>(
|
fun aRedactedMatrixTimeline(eventId: EventId) = listOf<MatrixTimelineItem>(
|
||||||
MatrixTimelineItem.Event(
|
MatrixTimelineItem.Event(
|
||||||
uniqueId = 0,
|
uniqueId = "0",
|
||||||
event = EventTimelineItem(
|
event = EventTimelineItem(
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
transactionId = null,
|
transactionId = null,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ fun aPollTimeline(
|
||||||
return FakeMatrixTimeline(
|
return FakeMatrixTimeline(
|
||||||
initialTimelineItems = polls.map { entry ->
|
initialTimelineItems = polls.map { entry ->
|
||||||
MatrixTimelineItem.Event(
|
MatrixTimelineItem.Event(
|
||||||
entry.key.hashCode().toLong(),
|
entry.key.value,
|
||||||
anEventTimelineItem(
|
anEventTimelineItem(
|
||||||
eventId = entry.key,
|
eventId = entry.key,
|
||||||
content = entry.value,
|
content = entry.value,
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,12 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventTimeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
|
|
||||||
sealed interface MatrixTimelineItem {
|
sealed interface MatrixTimelineItem {
|
||||||
data class Event(val uniqueId: Long, val event: EventTimelineItem) : MatrixTimelineItem {
|
data class Event(val uniqueId: String, val event: EventTimelineItem) : MatrixTimelineItem {
|
||||||
val eventId: EventId? = event.eventId
|
val eventId: EventId? = event.eventId
|
||||||
val transactionId: TransactionId? = event.transactionId
|
val transactionId: TransactionId? = event.transactionId
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Virtual(val uniqueId: Long, val virtual: VirtualTimelineItem) : MatrixTimelineItem
|
data class Virtual(val uniqueId: String, val virtual: VirtualTimelineItem) : MatrixTimelineItem
|
||||||
data object Other : MatrixTimelineItem
|
data object Other : MatrixTimelineItem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ class MatrixTimelineItemMapper(
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun map(timelineItem: TimelineItem): MatrixTimelineItem = timelineItem.use {
|
fun map(timelineItem: TimelineItem): MatrixTimelineItem = timelineItem.use {
|
||||||
val uniqueId = timelineItem.uniqueId().toLong()
|
val uniqueId = timelineItem.uniqueId().toString()
|
||||||
val asEvent = it.asEvent()
|
val asEvent = it.asEvent()
|
||||||
if (asEvent != null) {
|
if (asEvent != null) {
|
||||||
val eventTimelineItem = eventTimelineItemMapper.map(asEvent)
|
val eventTimelineItem = eventTimelineItemMapper.map(asEvent)
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.getAndUpdate
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
@ -80,7 +80,6 @@ class RustMatrixTimeline(
|
||||||
lastLoginTimestamp = lastLoginTimestamp,
|
lastLoginTimestamp = lastLoginTimestamp,
|
||||||
isRoomEncrypted = matrixRoom.isEncrypted,
|
isRoomEncrypted = matrixRoom.isEncrypted,
|
||||||
isKeyBackupEnabled = isKeyBackupEnabled,
|
isKeyBackupEnabled = isKeyBackupEnabled,
|
||||||
paginationStateFlow = _paginationState,
|
|
||||||
dispatcher = dispatcher,
|
dispatcher = dispatcher,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -102,6 +101,11 @@ class RustMatrixTimeline(
|
||||||
|
|
||||||
override val paginationState: StateFlow<MatrixTimeline.PaginationState> = _paginationState.asStateFlow()
|
override val paginationState: StateFlow<MatrixTimeline.PaginationState> = _paginationState.asStateFlow()
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems.mapLatest { items ->
|
||||||
|
encryptedHistoryPostProcessor.process(items)
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Timber.d("Initialize timeline for room ${matrixRoom.roomId}")
|
Timber.d("Initialize timeline for room ${matrixRoom.roomId}")
|
||||||
|
|
||||||
|
|
@ -115,9 +119,9 @@ class RustMatrixTimeline(
|
||||||
postDiffs(diffs)
|
postDiffs(diffs)
|
||||||
}.launchIn(this)
|
}.launchIn(this)
|
||||||
|
|
||||||
innerTimeline.backPaginationStatusFlow()
|
paginationStateFlow()
|
||||||
.onEach {
|
.onEach {
|
||||||
postPaginationStatus(it)
|
_paginationState.value = it
|
||||||
}
|
}
|
||||||
.launchIn(this)
|
.launchIn(this)
|
||||||
|
|
||||||
|
|
@ -125,6 +129,44 @@ class RustMatrixTimeline(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun paginationStateFlow(): Flow<MatrixTimeline.PaginationState> {
|
||||||
|
return combine(
|
||||||
|
innerTimeline.backPaginationStatusFlow(),
|
||||||
|
timelineItems,
|
||||||
|
) { paginationStatus, filteredItems ->
|
||||||
|
if (filteredItems.hasEncryptionHistoryBanner()) {
|
||||||
|
return@combine MatrixTimeline.PaginationState(
|
||||||
|
isBackPaginating = false,
|
||||||
|
hasMoreToLoadBackwards = false,
|
||||||
|
beginningOfRoomReached = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
when (paginationStatus) {
|
||||||
|
BackPaginationStatus.IDLE -> {
|
||||||
|
MatrixTimeline.PaginationState(
|
||||||
|
isBackPaginating = false,
|
||||||
|
hasMoreToLoadBackwards = true,
|
||||||
|
beginningOfRoomReached = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
BackPaginationStatus.PAGINATING -> {
|
||||||
|
MatrixTimeline.PaginationState(
|
||||||
|
isBackPaginating = true,
|
||||||
|
hasMoreToLoadBackwards = true,
|
||||||
|
beginningOfRoomReached = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
BackPaginationStatus.TIMELINE_START_REACHED -> {
|
||||||
|
MatrixTimeline.PaginationState(
|
||||||
|
isBackPaginating = false,
|
||||||
|
hasMoreToLoadBackwards = false,
|
||||||
|
beginningOfRoomReached = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun fetchMembers() = withContext(dispatcher) {
|
private suspend fun fetchMembers() = withContext(dispatcher) {
|
||||||
initLatch.await()
|
initLatch.await()
|
||||||
try {
|
try {
|
||||||
|
|
@ -134,11 +176,6 @@ class RustMatrixTimeline(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems.mapLatest { items ->
|
|
||||||
encryptedHistoryPostProcessor.process(items)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun postItems(items: List<TimelineItem>) = coroutineScope {
|
private suspend fun postItems(items: List<TimelineItem>) = coroutineScope {
|
||||||
// Split the initial items in multiple list as there is no pagination in the cached data, so we can post timelineItems asap.
|
// Split the initial items in multiple list as there is no pagination in the cached data, so we can post timelineItems asap.
|
||||||
items.chunked(INITIAL_MAX_SIZE).reversed().forEach {
|
items.chunked(INITIAL_MAX_SIZE).reversed().forEach {
|
||||||
|
|
@ -154,39 +191,6 @@ class RustMatrixTimeline(
|
||||||
timelineDiffProcessor.postDiffs(diffs)
|
timelineDiffProcessor.postDiffs(diffs)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun postPaginationStatus(status: BackPaginationStatus) {
|
|
||||||
_paginationState.getAndUpdate { currentPaginationState ->
|
|
||||||
if (hasEncryptionHistoryBanner()) {
|
|
||||||
return@getAndUpdate currentPaginationState.copy(
|
|
||||||
isBackPaginating = false,
|
|
||||||
hasMoreToLoadBackwards = false,
|
|
||||||
beginningOfRoomReached = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
when (status) {
|
|
||||||
BackPaginationStatus.IDLE -> {
|
|
||||||
currentPaginationState.copy(
|
|
||||||
isBackPaginating = false,
|
|
||||||
hasMoreToLoadBackwards = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
BackPaginationStatus.PAGINATING -> {
|
|
||||||
currentPaginationState.copy(
|
|
||||||
isBackPaginating = true,
|
|
||||||
hasMoreToLoadBackwards = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
BackPaginationStatus.TIMELINE_START_REACHED -> {
|
|
||||||
currentPaginationState.copy(
|
|
||||||
isBackPaginating = false,
|
|
||||||
hasMoreToLoadBackwards = false,
|
|
||||||
beginningOfRoomReached = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> = withContext(dispatcher) {
|
override suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> = withContext(dispatcher) {
|
||||||
runCatching {
|
runCatching {
|
||||||
innerTimeline.fetchDetailsForEvent(eventId.value)
|
innerTimeline.fetchDetailsForEvent(eventId.value)
|
||||||
|
|
@ -251,8 +255,8 @@ class RustMatrixTimeline(
|
||||||
return _timelineItems.value.firstOrNull { (it as? MatrixTimelineItem.Event)?.eventId == eventId } as? MatrixTimelineItem.Event
|
return _timelineItems.value.firstOrNull { (it as? MatrixTimelineItem.Event)?.eventId == eventId } as? MatrixTimelineItem.Event
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hasEncryptionHistoryBanner(): Boolean {
|
private fun List<MatrixTimelineItem>.hasEncryptionHistoryBanner(): Boolean {
|
||||||
val firstItem = _timelineItems.value.firstOrNull()
|
val firstItem = firstOrNull()
|
||||||
return firstItem is MatrixTimelineItem.Virtual
|
return firstItem is MatrixTimelineItem.Virtual
|
||||||
&& firstItem.virtual is VirtualTimelineItem.EncryptedHistoryBanner
|
&& firstItem.virtual is VirtualTimelineItem.EncryptedHistoryBanner
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,9 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
||||||
|
|
||||||
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.virtual.VirtualTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.getAndUpdate
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
@ -31,26 +28,12 @@ class TimelineEncryptedHistoryPostProcessor(
|
||||||
private val lastLoginTimestamp: Date?,
|
private val lastLoginTimestamp: Date?,
|
||||||
private val isRoomEncrypted: Boolean,
|
private val isRoomEncrypted: Boolean,
|
||||||
private val isKeyBackupEnabled: Boolean,
|
private val isKeyBackupEnabled: Boolean,
|
||||||
private val paginationStateFlow: MutableStateFlow<MatrixTimeline.PaginationState>,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun process(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> = withContext(dispatcher) {
|
suspend fun process(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> = withContext(dispatcher) {
|
||||||
Timber.d("Process on Thread=${Thread.currentThread()}")
|
Timber.d("Process on Thread=${Thread.currentThread()}")
|
||||||
if (!isRoomEncrypted || isKeyBackupEnabled || lastLoginTimestamp == null) return@withContext items
|
if (!isRoomEncrypted || isKeyBackupEnabled || lastLoginTimestamp == null) return@withContext items
|
||||||
|
replaceWithEncryptionHistoryBannerIfNeeded(items)
|
||||||
val filteredItems = replaceWithEncryptionHistoryBannerIfNeeded(items)
|
|
||||||
// Disable back pagination
|
|
||||||
val wasFiltered = filteredItems !== items
|
|
||||||
if (wasFiltered) {
|
|
||||||
paginationStateFlow.getAndUpdate {
|
|
||||||
it.copy(
|
|
||||||
isBackPaginating = false,
|
|
||||||
hasMoreToLoadBackwards = false,
|
|
||||||
beginningOfRoomReached = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
filteredItems
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun replaceWithEncryptionHistoryBannerIfNeeded(list: List<MatrixTimelineItem>): List<MatrixTimelineItem> {
|
private fun replaceWithEncryptionHistoryBannerIfNeeded(list: List<MatrixTimelineItem>): List<MatrixTimelineItem> {
|
||||||
|
|
@ -62,7 +45,7 @@ class TimelineEncryptedHistoryPostProcessor(
|
||||||
}
|
}
|
||||||
return if (lastEncryptedHistoryBannerIndex >= 0) {
|
return if (lastEncryptedHistoryBannerIndex >= 0) {
|
||||||
val sublist = list.drop(lastEncryptedHistoryBannerIndex + 1).toMutableList()
|
val sublist = list.drop(lastEncryptedHistoryBannerIndex + 1).toMutableList()
|
||||||
sublist.add(0, MatrixTimelineItem.Virtual(0L, VirtualTimelineItem.EncryptedHistoryBanner))
|
sublist.add(0, MatrixTimelineItem.Virtual(VirtualTimelineItem.EncryptedHistoryBanner.toString(), VirtualTimelineItem.EncryptedHistoryBanner))
|
||||||
sublist
|
sublist
|
||||||
} else {
|
} else {
|
||||||
list
|
list
|
||||||
|
|
|
||||||
|
|
@ -17,17 +17,17 @@
|
||||||
package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
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.virtual.VirtualTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
|
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
|
||||||
|
|
||||||
class TimelineEncryptedHistoryPostProcessorTest {
|
class TimelineEncryptedHistoryPostProcessorTest {
|
||||||
|
|
||||||
private val defaultLastLoginTimestamp = Date(1_689_061_264L)
|
private val defaultLastLoginTimestamp = Date(1_689_061_264L)
|
||||||
|
|
@ -36,7 +36,7 @@ class TimelineEncryptedHistoryPostProcessorTest {
|
||||||
fun `given an unencrypted room, nothing is done`() = runTest {
|
fun `given an unencrypted room, nothing is done`() = runTest {
|
||||||
val processor = createPostProcessor(isRoomEncrypted = false)
|
val processor = createPostProcessor(isRoomEncrypted = false)
|
||||||
val items = listOf(
|
val items = listOf(
|
||||||
MatrixTimelineItem.Event(0L, anEventTimelineItem())
|
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
|
||||||
)
|
)
|
||||||
assertThat(processor.process(items)).isSameInstanceAs(items)
|
assertThat(processor.process(items)).isSameInstanceAs(items)
|
||||||
}
|
}
|
||||||
|
|
@ -45,7 +45,7 @@ class TimelineEncryptedHistoryPostProcessorTest {
|
||||||
fun `given an encrypted room, and key backup enabled, nothing is done`() = runTest {
|
fun `given an encrypted room, and key backup enabled, nothing is done`() = runTest {
|
||||||
val processor = createPostProcessor(isKeyBackupEnabled = true)
|
val processor = createPostProcessor(isKeyBackupEnabled = true)
|
||||||
val items = listOf(
|
val items = listOf(
|
||||||
MatrixTimelineItem.Event(0L, anEventTimelineItem())
|
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
|
||||||
)
|
)
|
||||||
assertThat(processor.process(items)).isSameInstanceAs(items)
|
assertThat(processor.process(items)).isSameInstanceAs(items)
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +54,7 @@ class TimelineEncryptedHistoryPostProcessorTest {
|
||||||
fun `given a null lastLoginTimestamp, nothing is done`() = runTest {
|
fun `given a null lastLoginTimestamp, nothing is done`() = runTest {
|
||||||
val processor = createPostProcessor(lastLoginTimestamp = null)
|
val processor = createPostProcessor(lastLoginTimestamp = null)
|
||||||
val items = listOf(
|
val items = listOf(
|
||||||
MatrixTimelineItem.Event(0L, anEventTimelineItem())
|
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
|
||||||
)
|
)
|
||||||
assertThat(processor.process(items)).isSameInstanceAs(items)
|
assertThat(processor.process(items)).isSameInstanceAs(items)
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +70,7 @@ class TimelineEncryptedHistoryPostProcessorTest {
|
||||||
fun `given a list with no items before lastLoginTimestamp, nothing is done`() = runTest {
|
fun `given a list with no items before lastLoginTimestamp, nothing is done`() = runTest {
|
||||||
val processor = createPostProcessor()
|
val processor = createPostProcessor()
|
||||||
val items = listOf(
|
val items = listOf(
|
||||||
MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1))
|
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1))
|
||||||
)
|
)
|
||||||
assertThat(processor.process(items)).isSameInstanceAs(items)
|
assertThat(processor.process(items)).isSameInstanceAs(items)
|
||||||
}
|
}
|
||||||
|
|
@ -79,49 +79,35 @@ class TimelineEncryptedHistoryPostProcessorTest {
|
||||||
fun `given a list with an item with equal timestamp as lastLoginTimestamp, it's replaced`() = runTest {
|
fun `given a list with an item with equal timestamp as lastLoginTimestamp, it's replaced`() = runTest {
|
||||||
val processor = createPostProcessor()
|
val processor = createPostProcessor()
|
||||||
val items = listOf(
|
val items = listOf(
|
||||||
MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time))
|
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time))
|
||||||
)
|
)
|
||||||
assertThat(processor.process(items))
|
assertThat(processor.process(items))
|
||||||
.isEqualTo(listOf(MatrixTimelineItem.Virtual(0L, VirtualTimelineItem.EncryptedHistoryBanner)))
|
.isEqualTo(listOf(MatrixTimelineItem.Virtual(VirtualTimelineItem.EncryptedHistoryBanner.toString(), VirtualTimelineItem.EncryptedHistoryBanner)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given a list with an item with a lower timestamp than lastLoginTimestamp, it's replaced`() = runTest {
|
fun `given a list with an item with a lower timestamp than lastLoginTimestamp, it's replaced`() = runTest {
|
||||||
val processor = createPostProcessor()
|
val processor = createPostProcessor()
|
||||||
val items = listOf(
|
val items = listOf(
|
||||||
MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1))
|
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1))
|
||||||
)
|
)
|
||||||
assertThat(processor.process(items)).isEqualTo(
|
assertThat(processor.process(items)).isEqualTo(
|
||||||
listOf(MatrixTimelineItem.Virtual(0L, VirtualTimelineItem.EncryptedHistoryBanner))
|
listOf(MatrixTimelineItem.Virtual(VirtualTimelineItem.EncryptedHistoryBanner.toString(), VirtualTimelineItem.EncryptedHistoryBanner))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `given a list with several with lower or equal timestamps than lastLoginTimestamp, they're replaced and the user can't back paginate`() = runTest {
|
fun `given a list with several with lower or equal timestamps than lastLoginTimestamp, then they're replaced`() = runTest {
|
||||||
val paginationStateFlow = MutableStateFlow(
|
val processor = createPostProcessor()
|
||||||
MatrixTimeline.PaginationState(
|
|
||||||
hasMoreToLoadBackwards = true,
|
|
||||||
isBackPaginating = false,
|
|
||||||
beginningOfRoomReached = false,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val processor = createPostProcessor(paginationStateFlow = paginationStateFlow)
|
|
||||||
val items = listOf(
|
val items = listOf(
|
||||||
MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1)),
|
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1)),
|
||||||
MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time)),
|
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time)),
|
||||||
MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1)),
|
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1)),
|
||||||
)
|
)
|
||||||
assertThat(processor.process(items)).isEqualTo(
|
assertThat(processor.process(items)).isEqualTo(
|
||||||
listOf(
|
listOf(
|
||||||
MatrixTimelineItem.Virtual(0L, VirtualTimelineItem.EncryptedHistoryBanner),
|
MatrixTimelineItem.Virtual(VirtualTimelineItem.EncryptedHistoryBanner.toString(), VirtualTimelineItem.EncryptedHistoryBanner),
|
||||||
MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1))
|
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1))
|
||||||
)
|
|
||||||
)
|
|
||||||
assertThat(paginationStateFlow.value).isEqualTo(
|
|
||||||
MatrixTimeline.PaginationState(
|
|
||||||
hasMoreToLoadBackwards = false,
|
|
||||||
isBackPaginating = false,
|
|
||||||
beginningOfRoomReached = false,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -130,19 +116,10 @@ class TimelineEncryptedHistoryPostProcessorTest {
|
||||||
lastLoginTimestamp: Date? = defaultLastLoginTimestamp,
|
lastLoginTimestamp: Date? = defaultLastLoginTimestamp,
|
||||||
isRoomEncrypted: Boolean = true,
|
isRoomEncrypted: Boolean = true,
|
||||||
isKeyBackupEnabled: Boolean = false,
|
isKeyBackupEnabled: Boolean = false,
|
||||||
paginationStateFlow: MutableStateFlow<MatrixTimeline.PaginationState> =
|
|
||||||
MutableStateFlow(
|
|
||||||
MatrixTimeline.PaginationState(
|
|
||||||
hasMoreToLoadBackwards = true,
|
|
||||||
isBackPaginating = false,
|
|
||||||
beginningOfRoomReached = false,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) = TimelineEncryptedHistoryPostProcessor(
|
) = TimelineEncryptedHistoryPostProcessor(
|
||||||
lastLoginTimestamp = lastLoginTimestamp,
|
lastLoginTimestamp = lastLoginTimestamp,
|
||||||
isRoomEncrypted = isRoomEncrypted,
|
isRoomEncrypted = isRoomEncrypted,
|
||||||
isKeyBackupEnabled = isKeyBackupEnabled,
|
isKeyBackupEnabled = isKeyBackupEnabled,
|
||||||
paginationStateFlow = paginationStateFlow,
|
|
||||||
dispatcher = StandardTestDispatcher(testScheduler)
|
dispatcher = StandardTestDispatcher(testScheduler)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue