Timeline : makes sure all tests are passing

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

View file

@ -32,7 +32,9 @@ data class TimelineState(
val focusedEventId : EventId?,
val focusRequestState: FocusRequestState,
val eventSink: (TimelineEvents) -> Unit,
)
){
val isTimelineEmpty = timelineItems.none { it is TimelineItem.Event }
}
sealed interface 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.TimelineItemEventContentProvider
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.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
@ -125,12 +126,12 @@ fun TimelineView(
reverseLayout = useReverseLayout,
contentPadding = PaddingValues(vertical = 8.dp),
) {
/*
item(key = UUID.randomUUID()) {
TypingNotificationView(state = typingNotificationState)
}
*/
if(state.isLive) {
item {
TypingNotificationView(state = typingNotificationState)
}
}
items(
items = state.timelineItems,
contentType = { timelineItem -> timelineItem.contentType() },
@ -165,7 +166,7 @@ fun TimelineView(
)
TimelineScrollHelper(
isTimelineEmpty = state.timelineItems.isEmpty(),
isTimelineEmpty = state.isTimelineEmpty,
lazyListState = lazyListState,
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
newEventState = state.newEventState,
@ -185,10 +186,6 @@ private fun FocusRequestStateView(
onClearFocusRequestState: () -> Unit,
modifier: Modifier = Modifier,
) {
BackHandler(enabled = focusRequestState is FocusRequestState.Fetching) {
onClearFocusRequestState()
}
when (focusRequestState) {
is FocusRequestState.Failure -> {
ErrorDialog(
@ -198,7 +195,7 @@ private fun FocusRequestStateView(
)
}
FocusRequestState.Fetching -> {
ProgressDialog(modifier = modifier)
ProgressDialog(modifier = modifier, onDismissRequest = onClearFocusRequestState)
}
is FocusRequestState.Cached, FocusRequestState.None -> Unit
}
@ -254,7 +251,6 @@ private fun BoxScope.TimelineScrollHelper(
}
LaunchedEffect(canAutoScroll, newEventState) {
Timber.d("TimelineScrollHelper - canAutoScroll: $canAutoScroll, newEventState: $newEventState")
val shouldScrollToBottom = isScrollFinished && (canAutoScroll || newEventState == NewEventState.FromMe)
if (shouldScrollToBottom) {
scrollToBottom()

View file

@ -21,15 +21,20 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
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.test.AN_EVENT_ID
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.timeline.FakeTimeline
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.test.runTest
import org.junit.Rule
import org.junit.Test
import java.lang.IllegalStateException
class ForwardMessagesPresenterTests {
@get:Rule
@ -37,7 +42,7 @@ class ForwardMessagesPresenterTests {
@Test
fun `present - initial state`() = runTest {
val presenter = aPresenter()
val presenter = aForwardMessagesPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -50,7 +55,14 @@ class ForwardMessagesPresenterTests {
@Test
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) {
presenter.present()
}.test {
@ -62,18 +74,23 @@ class ForwardMessagesPresenterTests {
val successfulForwardState = awaitItem()
assertThat(successfulForwardState.isForwarding).isFalse()
assertThat(successfulForwardState.forwardingSucceeded).isNotNull()
assert(forwardEventLambda).isCalledOnce()
}
}
@Test
fun `present - select a room and forward failed, then clear`() = runTest {
val room = FakeMatrixRoom()
val presenter = aPresenter(fakeMatrixRoom = room)
val forwardEventLambda = lambdaRecorder { _: EventId, _: List<RoomId> ->
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) {
presenter.present()
}.test {
// Test failed forwarding
room.givenForwardEventResult(Result.failure(Throwable("error")))
skipItems(1)
val summary = aRoomSummaryDetails()
presenter.onRoomSelected(listOf(summary.roomId))
@ -83,10 +100,11 @@ class ForwardMessagesPresenterTests {
// Then clear error
failedForwardState.eventSink(ForwardMessagesEvents.ClearError)
assertThat(awaitItem().error).isNull()
assert(forwardEventLambda).isCalledOnce()
}
}
private fun CoroutineScope.aPresenter(
private fun CoroutineScope.aForwardMessagesPresenter(
eventId: EventId = AN_EVENT_ID,
fakeMatrixRoom: FakeMatrixRoom = FakeMatrixRoom(),
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.timeline.MatrixTimelineItem
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 kotlinx.coroutines.flow.first
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.matrix.api.room.MatrixRoom
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.flow.flatMapConcat
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
class PollHistoryPresenter @Inject constructor(
private val room: MatrixRoom,
private val appCoroutineScope: CoroutineScope,
private val sendPollResponseAction: SendPollResponseAction,
private val endPollAction: EndPollAction,
private val pollHistoryItemFactory: PollHistoryItemsFactory,
private val timelineProvider: TimelineProvider,
) : Presenter<PollHistoryState> {
@Composable
override fun present(): PollHistoryState {
// TODO use room.rememberPollHistory() when working properly?
val timeline = room.liveTimeline
val timeline by timelineProvider.activeTimelineFlow().collectAsState()
val paginationState by timeline.paginationStatus(Timeline.PaginationDirection.BACKWARDS).collectAsState()
val pollHistoryItemsFlow = remember {
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.libraries.dateformatter.test.FakeDaySeparatorFormatter
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.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
@ -174,11 +175,11 @@ class PollHistoryPresenterTest {
),
): PollHistoryPresenter {
return PollHistoryPresenter(
room = room,
appCoroutineScope = appCoroutineScope,
sendPollResponseAction = sendPollResponseAction,
endPollAction = endPollAction,
pollHistoryItemFactory = pollHistoryItemFactory,
timelineProvider = LiveTimelineProvider(room),
)
}
}