Timeline : Scroll to end of timeline when sending a new message #1877
This commit is contained in:
parent
5606559568
commit
02e65e4eac
7 changed files with 107 additions and 38 deletions
1
changelog.d/1877.feature
Normal file
1
changelog.d/1877.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Scroll to end of timeline when sending a new message.
|
||||||
|
|
@ -30,6 +30,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import im.vector.app.features.analytics.plan.PollEnd
|
import im.vector.app.features.analytics.plan.PollEnd
|
||||||
import im.vector.app.features.analytics.plan.PollVote
|
import im.vector.app.features.analytics.plan.PollVote
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
import io.element.android.features.messages.impl.timeline.session.SessionState
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager
|
||||||
|
|
@ -89,7 +90,7 @@ class TimelinePresenter @Inject constructor(
|
||||||
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
|
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
|
||||||
|
|
||||||
val prevMostRecentItemId = rememberSaveable { mutableStateOf<String?>(null) }
|
val prevMostRecentItemId = rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
val hasNewItems = remember { mutableStateOf(false) }
|
val newItemState = remember { mutableStateOf(NewEventState.None) }
|
||||||
|
|
||||||
val sessionVerifiedStatus by verificationService.sessionVerifiedStatus.collectAsState()
|
val sessionVerifiedStatus by verificationService.sessionVerifiedStatus.collectAsState()
|
||||||
val keyBackupState by encryptionService.backupStateStateFlow.collectAsState()
|
val keyBackupState by encryptionService.backupStateStateFlow.collectAsState()
|
||||||
|
|
@ -112,7 +113,7 @@ class TimelinePresenter @Inject constructor(
|
||||||
is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId
|
is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId
|
||||||
is TimelineEvents.OnScrollFinished -> {
|
is TimelineEvents.OnScrollFinished -> {
|
||||||
if (event.firstIndex == 0) {
|
if (event.firstIndex == 0) {
|
||||||
hasNewItems.value = false
|
newItemState.value = NewEventState.None
|
||||||
}
|
}
|
||||||
appScope.sendReadReceiptIfNeeded(
|
appScope.sendReadReceiptIfNeeded(
|
||||||
firstVisibleIndex = event.firstIndex,
|
firstVisibleIndex = event.firstIndex,
|
||||||
|
|
@ -139,7 +140,7 @@ class TimelinePresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(timelineItems.size) {
|
LaunchedEffect(timelineItems.size) {
|
||||||
computeHasNewItems(timelineItems, prevMostRecentItemId, hasNewItems)
|
computeNewItemState(timelineItems, prevMostRecentItemId, newItemState)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
|
@ -171,7 +172,7 @@ class TimelinePresenter @Inject constructor(
|
||||||
paginationState = paginationState,
|
paginationState = paginationState,
|
||||||
timelineItems = timelineItems,
|
timelineItems = timelineItems,
|
||||||
showReadReceipts = readReceiptsEnabled,
|
showReadReceipts = readReceiptsEnabled,
|
||||||
hasNewItems = hasNewItems.value,
|
newEventState = newItemState.value,
|
||||||
sessionState = sessionState,
|
sessionState = sessionState,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
|
|
@ -180,22 +181,32 @@ class TimelinePresenter @Inject constructor(
|
||||||
/**
|
/**
|
||||||
* This method compute the hasNewItem state passed as a [MutableState] each time the timeline items size changes.
|
* This method compute the hasNewItem state passed as a [MutableState] each time the timeline items size changes.
|
||||||
* Basically, if we got new timeline event from sync or local, either from us or another user, we update the state so we tell we have new items.
|
* Basically, if we got new timeline event from sync or local, either from us or another user, we update the state so we tell we have new items.
|
||||||
* The state never goes back to false from this method, but need to be reset from somewhere else.
|
* The state never goes back to None from this method, but need to be reset from somewhere else.
|
||||||
*/
|
*/
|
||||||
private suspend fun computeHasNewItems(
|
private suspend fun computeNewItemState(
|
||||||
timelineItems: ImmutableList<TimelineItem>,
|
timelineItems: ImmutableList<TimelineItem>,
|
||||||
prevMostRecentItemId: MutableState<String?>,
|
prevMostRecentItemId: MutableState<String?>,
|
||||||
hasNewItemsState: MutableState<Boolean>
|
newEventState: MutableState<NewEventState>
|
||||||
) = withContext(dispatchers.computation) {
|
) = withContext(dispatchers.computation) {
|
||||||
|
// FromMe is prioritized over FromOther, so skip if we already have a FromMe
|
||||||
|
if (newEventState.value == NewEventState.FromMe) {
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
val newMostRecentItem = timelineItems.firstOrNull()
|
val newMostRecentItem = timelineItems.firstOrNull()
|
||||||
val prevMostRecentItemIdValue = prevMostRecentItemId.value
|
val prevMostRecentItemIdValue = prevMostRecentItemId.value
|
||||||
val newMostRecentItemId = newMostRecentItem?.identifier()
|
val newMostRecentItemId = newMostRecentItem?.identifier()
|
||||||
val hasNewItems = prevMostRecentItemIdValue != null &&
|
val hasNewEvent = prevMostRecentItemIdValue != null &&
|
||||||
newMostRecentItem is TimelineItem.Event &&
|
newMostRecentItem is TimelineItem.Event &&
|
||||||
newMostRecentItem.origin != TimelineItemEventOrigin.PAGINATION &&
|
newMostRecentItem.origin != TimelineItemEventOrigin.PAGINATION &&
|
||||||
newMostRecentItemId != prevMostRecentItemIdValue
|
newMostRecentItemId != prevMostRecentItemIdValue
|
||||||
if (hasNewItems) {
|
if (hasNewEvent) {
|
||||||
hasNewItemsState.value = true
|
val newMostRecentEvent = newMostRecentItem as? TimelineItem.Event
|
||||||
|
val fromMe = newMostRecentEvent?.localSendState != null
|
||||||
|
newEventState.value = if (fromMe) {
|
||||||
|
NewEventState.FromMe
|
||||||
|
} else {
|
||||||
|
NewEventState.FromOther
|
||||||
|
}
|
||||||
}
|
}
|
||||||
prevMostRecentItemId.value = newMostRecentItemId
|
prevMostRecentItemId.value = newMostRecentItemId
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.messages.impl.timeline
|
package io.element.android.features.messages.impl.timeline
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
import io.element.android.features.messages.impl.timeline.session.SessionState
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
@ -30,7 +31,7 @@ data class TimelineState(
|
||||||
val highlightedEventId: EventId?,
|
val highlightedEventId: EventId?,
|
||||||
val userHasPermissionToSendMessage: Boolean,
|
val userHasPermissionToSendMessage: Boolean,
|
||||||
val paginationState: MatrixTimeline.PaginationState,
|
val paginationState: MatrixTimeline.PaginationState,
|
||||||
val hasNewItems: Boolean,
|
val newEventState: NewEventState,
|
||||||
val sessionState: SessionState,
|
val sessionState: SessionState,
|
||||||
val eventSink: (TimelineEvents) -> Unit
|
val eventSink: (TimelineEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.features.messages.impl.timeline
|
package io.element.android.features.messages.impl.timeline
|
||||||
|
|
||||||
import io.element.android.features.messages.impl.timeline.model.InReplyToDetails
|
import io.element.android.features.messages.impl.timeline.model.InReplyToDetails
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
import io.element.android.features.messages.impl.timeline.model.ReadReceiptData
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
import io.element.android.features.messages.impl.timeline.model.TimelineItemGroupPosition
|
||||||
|
|
@ -53,7 +54,7 @@ fun aTimelineState(timelineItems: ImmutableList<TimelineItem> = persistentListOf
|
||||||
),
|
),
|
||||||
highlightedEventId = null,
|
highlightedEventId = null,
|
||||||
userHasPermissionToSendMessage = true,
|
userHasPermissionToSendMessage = true,
|
||||||
hasNewItems = false,
|
newEventState = NewEventState.None,
|
||||||
sessionState = aSessionState(
|
sessionState = aSessionState(
|
||||||
isSessionVerified = true,
|
isSessionVerified = true,
|
||||||
isKeyBackupEnabled = true,
|
isKeyBackupEnabled = true,
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ import io.element.android.features.messages.impl.timeline.components.virtual.Tim
|
||||||
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator
|
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator
|
||||||
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
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
|
||||||
|
|
@ -167,7 +168,7 @@ fun TimelineView(
|
||||||
TimelineScrollHelper(
|
TimelineScrollHelper(
|
||||||
isTimelineEmpty = state.timelineItems.isEmpty(),
|
isTimelineEmpty = state.timelineItems.isEmpty(),
|
||||||
lazyListState = lazyListState,
|
lazyListState = lazyListState,
|
||||||
hasNewItems = state.hasNewItems,
|
newEventState = state.newEventState,
|
||||||
onScrollFinishedAt = ::onScrollFinishedAt
|
onScrollFinishedAt = ::onScrollFinishedAt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -286,22 +287,34 @@ private fun TimelineItemRow(
|
||||||
private fun BoxScope.TimelineScrollHelper(
|
private fun BoxScope.TimelineScrollHelper(
|
||||||
isTimelineEmpty: Boolean,
|
isTimelineEmpty: Boolean,
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
hasNewItems: Boolean,
|
newEventState: NewEventState,
|
||||||
onScrollFinishedAt: (Int) -> Unit,
|
onScrollFinishedAt: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val isScrollFinished by remember { derivedStateOf { !lazyListState.isScrollInProgress } }
|
val isScrollFinished by remember { derivedStateOf { !lazyListState.isScrollInProgress } }
|
||||||
val canAutoScroll by remember { derivedStateOf { lazyListState.firstVisibleItemIndex < 3 } }
|
val canAutoScroll by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
lazyListState.firstVisibleItemIndex < 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(canAutoScroll, hasNewItems) {
|
fun scrollToBottom() {
|
||||||
val shouldAutoScroll = isScrollFinished && canAutoScroll && hasNewItems
|
coroutineScope.launch {
|
||||||
if (shouldAutoScroll) {
|
if (lazyListState.firstVisibleItemIndex > 10) {
|
||||||
coroutineScope.launch {
|
lazyListState.scrollToItem(0)
|
||||||
|
} else {
|
||||||
lazyListState.animateScrollToItem(0)
|
lazyListState.animateScrollToItem(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(canAutoScroll, newEventState) {
|
||||||
|
val shouldAutoScroll = isScrollFinished && (canAutoScroll || newEventState == NewEventState.FromMe)
|
||||||
|
if (shouldAutoScroll) {
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(isScrollFinished, isTimelineEmpty) {
|
LaunchedEffect(isScrollFinished, isTimelineEmpty) {
|
||||||
if (isScrollFinished && !isTimelineEmpty) {
|
if (isScrollFinished && !isTimelineEmpty) {
|
||||||
// Notify the parent composable about the first visible item index when scrolling finishes
|
// Notify the parent composable about the first visible item index when scrolling finishes
|
||||||
|
|
@ -315,15 +328,7 @@ private fun BoxScope.TimelineScrollHelper(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(end = 24.dp, bottom = 12.dp),
|
.padding(end = 24.dp, bottom = 12.dp),
|
||||||
onClick = {
|
onClick = ::scrollToBottom,
|
||||||
coroutineScope.launch {
|
|
||||||
if (lazyListState.firstVisibleItemIndex > 10) {
|
|
||||||
lazyListState.scrollToItem(0)
|
|
||||||
} else {
|
|
||||||
lazyListState.animateScrollToItem(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model if there is a new event in the timeline and if it is from me or from other.
|
||||||
|
* This can be used to scroll to the bottom of the list when a new event is added.
|
||||||
|
*/
|
||||||
|
enum class NewEventState {
|
||||||
|
None, FromMe, FromOther
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,7 @@ import im.vector.app.features.analytics.plan.PollVote
|
||||||
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
||||||
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory
|
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.session.SessionState
|
import io.element.android.features.messages.impl.timeline.session.SessionState
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager
|
import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager
|
||||||
|
|
@ -35,6 +36,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
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.LocalEventSendState
|
||||||
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.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
|
||||||
|
|
@ -47,7 +49,9 @@ import io.element.android.libraries.matrix.test.verification.FakeSessionVerifica
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
import io.element.android.tests.testutils.awaitWithLatch
|
import io.element.android.tests.testutils.awaitWithLatch
|
||||||
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import io.element.android.tests.testutils.waitForPredicate
|
import io.element.android.tests.testutils.waitForPredicate
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
@ -186,28 +190,49 @@ class TimelinePresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - covers hasNewItems scenarios`() = runTest {
|
fun `present - covers newEventState scenarios`() = runTest {
|
||||||
val timeline = FakeMatrixTimeline()
|
val timeline = FakeMatrixTimeline()
|
||||||
val presenter = createTimelinePresenter(timeline)
|
val presenter = createTimelinePresenter(timeline)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.hasNewItems).isFalse()
|
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())))
|
||||||
}
|
}
|
||||||
skipItems(1)
|
consumeItemsUntilPredicate { it.timelineItems.size == 1 }
|
||||||
assertThat(awaitItem().timelineItems.size).isEqualTo(1)
|
// Mimics sending a message, and assert newEventState is FromMe
|
||||||
timeline.updateTimelineItems { items ->
|
timeline.updateTimelineItems { items ->
|
||||||
items + listOf(MatrixTimelineItem.Event(1, anEventTimelineItem(content = aMessageContent())))
|
val event = anEventTimelineItem(content = aMessageContent(), localSendState = LocalEventSendState.Sent(AN_EVENT_ID))
|
||||||
|
items + listOf(MatrixTimelineItem.Event(1, event))
|
||||||
}
|
}
|
||||||
skipItems(1)
|
consumeItemsUntilPredicate { it.timelineItems.size == 2 }
|
||||||
assertThat(awaitItem().timelineItems.size).isEqualTo(2)
|
awaitLastSequentialItem().also { state ->
|
||||||
assertThat(awaitItem().hasNewItems).isTrue()
|
assertThat(state.newEventState).isEqualTo(NewEventState.FromMe)
|
||||||
|
}
|
||||||
|
// Mimics receiving a message without clearing the previous FromMe
|
||||||
|
timeline.updateTimelineItems { items ->
|
||||||
|
val event = anEventTimelineItem(content = aMessageContent())
|
||||||
|
items + listOf(MatrixTimelineItem.Event(2, event))
|
||||||
|
}
|
||||||
|
consumeItemsUntilPredicate { it.timelineItems.size == 3 }
|
||||||
|
|
||||||
|
// Scroll to bottom to clear previous FromMe
|
||||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
||||||
assertThat(awaitItem().hasNewItems).isFalse()
|
awaitLastSequentialItem().also { state ->
|
||||||
|
assertThat(state.newEventState).isEqualTo(NewEventState.None)
|
||||||
|
}
|
||||||
|
// Mimics receiving a message and assert newEventState is FromOther
|
||||||
|
timeline.updateTimelineItems { items ->
|
||||||
|
val event = anEventTimelineItem(content = aMessageContent())
|
||||||
|
items + listOf(MatrixTimelineItem.Event(3, event))
|
||||||
|
}
|
||||||
|
consumeItemsUntilPredicate { it.timelineItems.size == 4 }
|
||||||
|
awaitLastSequentialItem().also { state ->
|
||||||
|
assertThat(state.newEventState).isEqualTo(NewEventState.FromOther)
|
||||||
|
}
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -220,7 +245,7 @@ class TimelinePresenterTest {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.hasNewItems).isFalse()
|
assertThat(initialState.newEventState).isEqualTo(NewEventState.None)
|
||||||
assertThat(initialState.timelineItems.size).isEqualTo(0)
|
assertThat(initialState.timelineItems.size).isEqualTo(0)
|
||||||
val now = Date().time
|
val now = Date().time
|
||||||
val minuteInMilis = 60 * 1000
|
val minuteInMilis = 60 * 1000
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue