Timeline : makes sure all tests are passing

This commit is contained in:
ganfra 2024-04-25 14:35:37 +02:00
parent 9ffed34303
commit cf1c728eab
12 changed files with 108 additions and 49 deletions

View file

@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.TimelineProvider import io.element.android.libraries.matrix.api.timeline.TimelineProvider
import io.element.android.libraries.matrix.api.timeline.getActiveTimeline
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope

View file

@ -22,17 +22,22 @@ import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.LiveTimelineProvider import io.element.android.libraries.matrix.api.timeline.LiveTimelineProvider
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
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.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import java.io.Closeable import java.io.Closeable
import java.util.Optional import java.util.Optional
import javax.inject.Inject import javax.inject.Inject
@ -49,12 +54,14 @@ class TimelineController @Inject constructor(
private val room: MatrixRoom, private val room: MatrixRoom,
) : Closeable, TimelineProvider { ) : Closeable, TimelineProvider {
private val coroutineScope = CoroutineScope(SupervisorJob())
private val liveTimeline = MutableStateFlow(room.liveTimeline) private val liveTimeline = MutableStateFlow(room.liveTimeline)
private val detachedTimeline = MutableStateFlow<Optional<Timeline>>(Optional.empty()) private val detachedTimeline = MutableStateFlow<Optional<Timeline>>(Optional.empty())
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
fun timelineItems(): Flow<List<MatrixTimelineItem>> { fun timelineItems(): Flow<List<MatrixTimelineItem>> {
return currentTimelineFlow().flatMapLatest { it.timelineItems } return currentTimelineFlow.flatMapLatest { it.timelineItems }
} }
fun isLive(): Flow<Boolean> { fun isLive(): Flow<Boolean> {
@ -62,7 +69,7 @@ class TimelineController @Inject constructor(
} }
suspend fun invokeOnCurrentTimeline(block: suspend (Timeline.() -> Any)) { suspend fun invokeOnCurrentTimeline(block: suspend (Timeline.() -> Any)) {
currentTimelineFlow().first().run { currentTimelineFlow.value.run {
block(this) block(this)
} }
} }
@ -89,6 +96,10 @@ class TimelineController @Inject constructor(
* This does close the detached timeline if any. * This does close the detached timeline if any.
*/ */
fun focusOnLive() { fun focusOnLive() {
closeDetachedTimeline()
}
private fun closeDetachedTimeline() {
detachedTimeline.getAndUpdate { detachedTimeline.getAndUpdate {
when { when {
it.isPresent -> { it.isPresent -> {
@ -101,11 +112,12 @@ class TimelineController @Inject constructor(
} }
override fun close() { override fun close() {
focusOnLive() coroutineScope.cancel()
closeDetachedTimeline()
} }
suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> { suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> {
return currentTimelineFlow().first().paginate(direction) return currentTimelineFlow.value.paginate(direction)
.onSuccess { hasReachedEnd -> .onSuccess { hasReachedEnd ->
if (direction == Timeline.PaginationDirection.FORWARDS && hasReachedEnd) { if (direction == Timeline.PaginationDirection.FORWARDS && hasReachedEnd) {
focusOnLive() focusOnLive()
@ -113,14 +125,14 @@ class TimelineController @Inject constructor(
} }
} }
private fun currentTimelineFlow() = combine(liveTimeline, detachedTimeline) { live, detached -> private val currentTimelineFlow = combine(liveTimeline, detachedTimeline) { live, detached ->
when { when {
detached.isPresent -> detached.get() detached.isPresent -> detached.get()
else -> live else -> live
} }
} }.stateIn(coroutineScope, SharingStarted.Eagerly, room.liveTimeline)
override suspend fun getActiveTimeline(): Timeline { override fun activeTimelineFlow(): StateFlow<Timeline> {
return currentTimelineFlow().first() return currentTimelineFlow
} }
} }

View file

@ -32,7 +32,9 @@ data class TimelineState(
val focusedEventId : EventId?, val focusedEventId : EventId?,
val focusRequestState: FocusRequestState, val focusRequestState: FocusRequestState,
val eventSink: (TimelineEvents) -> Unit, val eventSink: (TimelineEvents) -> Unit,
) ){
val isTimelineEmpty = timelineItems.none { it is TimelineItem.Event }
}
sealed interface FocusRequestState { sealed interface FocusRequestState {
data object None : FocusRequestState data object None : FocusRequestState

View file

@ -63,6 +63,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
import io.element.android.features.messages.impl.typing.TypingNotificationState import io.element.android.features.messages.impl.typing.TypingNotificationState
import io.element.android.features.messages.impl.typing.TypingNotificationView
import io.element.android.features.messages.impl.typing.aTypingNotificationState import io.element.android.features.messages.impl.typing.aTypingNotificationState
import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
@ -125,12 +126,12 @@ fun TimelineView(
reverseLayout = useReverseLayout, reverseLayout = useReverseLayout,
contentPadding = PaddingValues(vertical = 8.dp), contentPadding = PaddingValues(vertical = 8.dp),
) { ) {
/*
item(key = UUID.randomUUID()) {
TypingNotificationView(state = typingNotificationState)
}
*/ if(state.isLive) {
item {
TypingNotificationView(state = typingNotificationState)
}
}
items( items(
items = state.timelineItems, items = state.timelineItems,
contentType = { timelineItem -> timelineItem.contentType() }, contentType = { timelineItem -> timelineItem.contentType() },
@ -165,7 +166,7 @@ fun TimelineView(
) )
TimelineScrollHelper( TimelineScrollHelper(
isTimelineEmpty = state.timelineItems.isEmpty(), isTimelineEmpty = state.isTimelineEmpty,
lazyListState = lazyListState, lazyListState = lazyListState,
forceJumpToBottomVisibility = forceJumpToBottomVisibility, forceJumpToBottomVisibility = forceJumpToBottomVisibility,
newEventState = state.newEventState, newEventState = state.newEventState,
@ -185,10 +186,6 @@ private fun FocusRequestStateView(
onClearFocusRequestState: () -> Unit, onClearFocusRequestState: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
BackHandler(enabled = focusRequestState is FocusRequestState.Fetching) {
onClearFocusRequestState()
}
when (focusRequestState) { when (focusRequestState) {
is FocusRequestState.Failure -> { is FocusRequestState.Failure -> {
ErrorDialog( ErrorDialog(
@ -198,7 +195,7 @@ private fun FocusRequestStateView(
) )
} }
FocusRequestState.Fetching -> { FocusRequestState.Fetching -> {
ProgressDialog(modifier = modifier) ProgressDialog(modifier = modifier, onDismissRequest = onClearFocusRequestState)
} }
is FocusRequestState.Cached, FocusRequestState.None -> Unit is FocusRequestState.Cached, FocusRequestState.None -> Unit
} }
@ -254,7 +251,6 @@ private fun BoxScope.TimelineScrollHelper(
} }
LaunchedEffect(canAutoScroll, newEventState) { LaunchedEffect(canAutoScroll, newEventState) {
Timber.d("TimelineScrollHelper - canAutoScroll: $canAutoScroll, newEventState: $newEventState")
val shouldScrollToBottom = isScrollFinished && (canAutoScroll || newEventState == NewEventState.FromMe) val shouldScrollToBottom = isScrollFinished && (canAutoScroll || newEventState == NewEventState.FromMe)
if (shouldScrollToBottom) { if (shouldScrollToBottom) {
scrollToBottom() scrollToBottom()

View file

@ -21,15 +21,20 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.timeline.LiveTimelineProvider import io.element.android.libraries.matrix.api.timeline.LiveTimelineProvider
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.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import java.lang.IllegalStateException
class ForwardMessagesPresenterTests { class ForwardMessagesPresenterTests {
@get:Rule @get:Rule
@ -37,7 +42,7 @@ class ForwardMessagesPresenterTests {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val presenter = aPresenter() val presenter = aForwardMessagesPresenter()
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -50,7 +55,14 @@ class ForwardMessagesPresenterTests {
@Test @Test
fun `present - forward successful`() = runTest { fun `present - forward successful`() = runTest {
val presenter = aPresenter() val forwardEventLambda = lambdaRecorder { _: EventId, _: List<RoomId> ->
Result.success(Unit)
}
val timeline = FakeTimeline().apply {
this.forwardEventLambda = forwardEventLambda
}
val room = FakeMatrixRoom(liveTimeline = timeline)
val presenter = aForwardMessagesPresenter(fakeMatrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
@ -62,18 +74,23 @@ class ForwardMessagesPresenterTests {
val successfulForwardState = awaitItem() val successfulForwardState = awaitItem()
assertThat(successfulForwardState.isForwarding).isFalse() assertThat(successfulForwardState.isForwarding).isFalse()
assertThat(successfulForwardState.forwardingSucceeded).isNotNull() assertThat(successfulForwardState.forwardingSucceeded).isNotNull()
assert(forwardEventLambda).isCalledOnce()
} }
} }
@Test @Test
fun `present - select a room and forward failed, then clear`() = runTest { fun `present - select a room and forward failed, then clear`() = runTest {
val room = FakeMatrixRoom() val forwardEventLambda = lambdaRecorder { _: EventId, _: List<RoomId> ->
val presenter = aPresenter(fakeMatrixRoom = room) Result.failure<Unit>(IllegalStateException("error"))
}
val timeline = FakeTimeline().apply {
this.forwardEventLambda = forwardEventLambda
}
val room = FakeMatrixRoom(liveTimeline = timeline)
val presenter = aForwardMessagesPresenter(fakeMatrixRoom = room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
// Test failed forwarding
room.givenForwardEventResult(Result.failure(Throwable("error")))
skipItems(1) skipItems(1)
val summary = aRoomSummaryDetails() val summary = aRoomSummaryDetails()
presenter.onRoomSelected(listOf(summary.roomId)) presenter.onRoomSelected(listOf(summary.roomId))
@ -83,10 +100,11 @@ class ForwardMessagesPresenterTests {
// Then clear error // Then clear error
failedForwardState.eventSink(ForwardMessagesEvents.ClearError) failedForwardState.eventSink(ForwardMessagesEvents.ClearError)
assertThat(awaitItem().error).isNull() assertThat(awaitItem().error).isNull()
assert(forwardEventLambda).isCalledOnce()
} }
} }
private fun CoroutineScope.aPresenter( private fun CoroutineScope.aForwardMessagesPresenter(
eventId: EventId = AN_EVENT_ID, eventId: EventId = AN_EVENT_ID,
fakeMatrixRoom: FakeMatrixRoom = FakeMatrixRoom(), fakeMatrixRoom: FakeMatrixRoom = FakeMatrixRoom(),
coroutineScope: CoroutineScope = this, coroutineScope: CoroutineScope = this,

View file

@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
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.TimelineProvider import io.element.android.libraries.matrix.api.timeline.TimelineProvider
import io.element.android.libraries.matrix.api.timeline.getActiveTimeline
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import javax.inject.Inject import javax.inject.Inject

View file

@ -34,22 +34,23 @@ import io.element.android.features.poll.impl.history.model.PollHistoryItemsFacto
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
class PollHistoryPresenter @Inject constructor( class PollHistoryPresenter @Inject constructor(
private val room: MatrixRoom,
private val appCoroutineScope: CoroutineScope, private val appCoroutineScope: CoroutineScope,
private val sendPollResponseAction: SendPollResponseAction, private val sendPollResponseAction: SendPollResponseAction,
private val endPollAction: EndPollAction, private val endPollAction: EndPollAction,
private val pollHistoryItemFactory: PollHistoryItemsFactory, private val pollHistoryItemFactory: PollHistoryItemsFactory,
private val timelineProvider: TimelineProvider,
) : Presenter<PollHistoryState> { ) : Presenter<PollHistoryState> {
@Composable @Composable
override fun present(): PollHistoryState { override fun present(): PollHistoryState {
// TODO use room.rememberPollHistory() when working properly? val timeline by timelineProvider.activeTimelineFlow().collectAsState()
val timeline = room.liveTimeline
val paginationState by timeline.paginationStatus(Timeline.PaginationDirection.BACKWARDS).collectAsState() val paginationState by timeline.paginationStatus(Timeline.PaginationDirection.BACKWARDS).collectAsState()
val pollHistoryItemsFlow = remember { val pollHistoryItemsFlow = remember {
timeline.timelineItems.map { items -> timeline.timelineItems.map { items ->

View file

@ -32,6 +32,7 @@ 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.dateformatter.test.FakeDaySeparatorFormatter import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.LiveTimelineProvider
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
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.AN_EVENT_ID_2 import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
@ -174,11 +175,11 @@ class PollHistoryPresenterTest {
), ),
): PollHistoryPresenter { ): PollHistoryPresenter {
return PollHistoryPresenter( return PollHistoryPresenter(
room = room,
appCoroutineScope = appCoroutineScope, appCoroutineScope = appCoroutineScope,
sendPollResponseAction = sendPollResponseAction, sendPollResponseAction = sendPollResponseAction,
endPollAction = endPollAction, endPollAction = endPollAction,
pollHistoryItemFactory = pollHistoryItemFactory, pollHistoryItemFactory = pollHistoryItemFactory,
timelineProvider = LiveTimelineProvider(room),
) )
} }
} }

View file

@ -19,6 +19,11 @@ package io.element.android.libraries.matrix.api.timeline
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -26,9 +31,11 @@ import javax.inject.Inject
* It could be the current room timeline, or a timeline for a specific event. * It could be the current room timeline, or a timeline for a specific event.
*/ */
interface TimelineProvider { interface TimelineProvider {
suspend fun getActiveTimeline(): Timeline fun activeTimelineFlow(): StateFlow<Timeline>
} }
suspend fun TimelineProvider.getActiveTimeline(): Timeline = activeTimelineFlow().first()
/** /**
* Default implementation of [TimelineProvider] that provides the live timeline of a room. * Default implementation of [TimelineProvider] that provides the live timeline of a room.
*/ */
@ -36,6 +43,6 @@ interface TimelineProvider {
class LiveTimelineProvider @Inject constructor( class LiveTimelineProvider @Inject constructor(
private val room: MatrixRoom, private val room: MatrixRoom,
) : TimelineProvider { ) : TimelineProvider {
override suspend fun getActiveTimeline(): Timeline = room.liveTimeline override fun activeTimelineFlow(): StateFlow<Timeline> = MutableStateFlow(room.liveTimeline)
} }

View file

@ -19,7 +19,7 @@ package io.element.android.libraries.matrix.impl.timeline.postprocessor
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
fun List<MatrixTimelineItem>.hasEncryptionHistoryBanner(): Boolean { internal fun List<MatrixTimelineItem>.hasEncryptionHistoryBanner(): Boolean {
val firstItem = firstOrNull() val firstItem = firstOrNull()
return firstItem is MatrixTimelineItem.Virtual && return firstItem is MatrixTimelineItem.Virtual &&
firstItem.virtual is VirtualTimelineItem.EncryptedHistoryBanner firstItem.virtual is VirtualTimelineItem.EncryptedHistoryBanner

View file

@ -16,6 +16,7 @@
package io.element.android.libraries.matrix.impl.timeline.postprocessor package io.element.android.libraries.matrix.impl.timeline.postprocessor
import androidx.annotation.VisibleForTesting
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.MembershipChange import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
@ -43,10 +44,7 @@ class RoomBeginningPostProcessor {
private fun processForRoom(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> { private fun processForRoom(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> {
if (items.hasEncryptionHistoryBanner()) return items if (items.hasEncryptionHistoryBanner()) return items
val roomBeginningItem = MatrixTimelineItem.Virtual( val roomBeginningItem = createRoomBeginningItem()
uniqueId = VirtualTimelineItem.RoomBeginning.toString(),
virtual = VirtualTimelineItem.RoomBeginning
)
return listOf(roomBeginningItem) + items return listOf(roomBeginningItem) + items
} }
@ -77,4 +75,13 @@ class RoomBeginningPostProcessor {
} }
return newItems return newItems
} }
@VisibleForTesting
fun createRoomBeginningItem(): MatrixTimelineItem.Virtual {
return MatrixTimelineItem.Virtual(
uniqueId = VirtualTimelineItem.RoomBeginning.toString(),
virtual = VirtualTimelineItem.RoomBeginning
)
}
} }

View file

@ -22,13 +22,14 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MembershipCha
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.libraries.matrix.test.A_USER_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.A_USER_ID_2
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
import org.junit.Test import org.junit.Test
class DmBeginningTimelineProcessorTest { class RoomBeginningPostProcessorTest {
@Test @Test
fun `processor removes room creation event and self-join event from DM timeline`() { fun `processor removes room creation event and self-join event from DM timeline`() {
val timelineItems = listOf( val timelineItems = listOf(
@ -36,7 +37,7 @@ class DmBeginningTimelineProcessorTest {
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))), MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
) )
val processor = RoomBeginningPostProcessor() val processor = RoomBeginningPostProcessor()
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false)
assertThat(processedItems).isEmpty() assertThat(processedItems).isEmpty()
} }
@ -53,18 +54,30 @@ class DmBeginningTimelineProcessorTest {
MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))), MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))),
) )
val processor = RoomBeginningPostProcessor() val processor = RoomBeginningPostProcessor()
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true) val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false)
assertThat(processedItems).isEqualTo(expected) assertThat(processedItems).isEqualTo(expected)
} }
@Test @Test
fun `processor won't remove items if it's not a DM`() { fun `processor will add beginning of room item if it's not a DM`() {
val timelineItems = listOf( val timelineItems = listOf(
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))), MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))), MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
) )
val processor = RoomBeginningPostProcessor() val processor = RoomBeginningPostProcessor()
val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = true) val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false)
assertThat(processedItems).isEqualTo(
listOf(processor.createRoomBeginningItem()) + timelineItems
)
}
@Test
fun `processor will not add beginning of room item if it's not a DM and EncryptedHistoryBanner item is found`() {
val timelineItems = listOf(
MatrixTimelineItem.Virtual("EncryptedHistoryBanner", VirtualTimelineItem.EncryptedHistoryBanner),
)
val processor = RoomBeginningPostProcessor()
val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = false)
assertThat(processedItems).isEqualTo(timelineItems) assertThat(processedItems).isEqualTo(timelineItems)
} }
@ -75,7 +88,7 @@ class DmBeginningTimelineProcessorTest {
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))), MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
) )
val processor = RoomBeginningPostProcessor() val processor = RoomBeginningPostProcessor()
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false) val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
assertThat(processedItems).isEqualTo(timelineItems) assertThat(processedItems).isEqualTo(timelineItems)
} }
@ -85,7 +98,7 @@ class DmBeginningTimelineProcessorTest {
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))), MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
) )
val processor = RoomBeginningPostProcessor() val processor = RoomBeginningPostProcessor()
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false) val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
assertThat(processedItems).isEqualTo(timelineItems) assertThat(processedItems).isEqualTo(timelineItems)
} }
@ -96,7 +109,7 @@ class DmBeginningTimelineProcessorTest {
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))), MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))),
) )
val processor = RoomBeginningPostProcessor() val processor = RoomBeginningPostProcessor()
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false) val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
assertThat(processedItems).isEqualTo(timelineItems) assertThat(processedItems).isEqualTo(timelineItems)
} }
} }