diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelinePresenter.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelinePresenter.kt index a468072950..b2cda24ff2 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelinePresenter.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelinePresenter.kt @@ -89,6 +89,7 @@ class TimelinePresenter @Inject constructor( return TimelineState( highlightedEventId = highlightedEventId.value, + paginationState = paginationState.value, timelineItems = timelineItems.value.toImmutableList(), eventSink = ::handleEvents ) diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineState.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineState.kt index c86dab1422..affe32ee66 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineState.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineState.kt @@ -19,11 +19,13 @@ package io.element.android.features.messages.timeline import androidx.compose.runtime.Immutable import io.element.android.features.messages.timeline.model.TimelineItem import io.element.android.libraries.matrix.core.EventId +import io.element.android.libraries.matrix.timeline.MatrixTimeline import kotlinx.collections.immutable.ImmutableList @Immutable data class TimelineState( val timelineItems: ImmutableList, val highlightedEventId: EventId?, + val paginationState: MatrixTimeline.PaginationState, val eventSink: (TimelineEvents) -> Unit ) diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineStateProvider.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineStateProvider.kt index 5286e73d6c..6f3c04a7c6 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineStateProvider.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/TimelineStateProvider.kt @@ -24,11 +24,13 @@ import io.element.android.features.messages.timeline.model.event.TimelineItemEve import io.element.android.features.messages.timeline.model.event.TimelineItemTextContent import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.core.EventId +import io.element.android.libraries.matrix.timeline.MatrixTimeline import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf fun aTimelineState() = TimelineState( timelineItems = persistentListOf(), + paginationState = MatrixTimeline.PaginationState(isBackPaginating = false, canBackPaginate = true), highlightedEventId = null, eventSink = {} ) diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemEventFactory.kt index e68aa331dc..4dbf72b8e2 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemEventFactory.kt @@ -22,14 +22,12 @@ import io.element.android.features.messages.timeline.model.TimelineItemGroupPosi import io.element.android.features.messages.timeline.model.TimelineItemReactions import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize -import io.element.android.libraries.matrix.room.MatrixRoom import io.element.android.libraries.matrix.timeline.MatrixTimelineItem import kotlinx.collections.immutable.toImmutableList import org.matrix.rustcomponents.sdk.ProfileTimelineDetails import javax.inject.Inject class TimelineItemEventFactory @Inject constructor( - private val room: MatrixRoom, private val contentFactory: TimelineItemContentFactory, ) { diff --git a/features/messages/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index f7c675430a..49786d1c49 100644 --- a/features/messages/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -24,12 +24,14 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.actionlist.ActionListPresenter import io.element.android.features.messages.actionlist.model.TimelineItemAction +import io.element.android.features.messages.fixtures.aMessageEvent +import io.element.android.features.messages.fixtures.aTimelineItemsFactory import io.element.android.features.messages.textcomposer.MessageComposerPresenter import io.element.android.features.messages.timeline.TimelinePresenter import io.element.android.features.messages.timeline.model.TimelineItem import io.element.android.features.messages.timeline.model.TimelineItemReactions -import io.element.android.features.messages.timeline.model.content.TimelineItemContent -import io.element.android.features.messages.timeline.model.content.TimelineItemTextContent +import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent +import io.element.android.features.messages.timeline.model.event.TimelineItemTextContent import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.room.MatrixRoom @@ -38,7 +40,6 @@ import io.element.android.libraries.matrixtest.A_MESSAGE import io.element.android.libraries.matrixtest.A_ROOM_ID import io.element.android.libraries.matrixtest.A_USER_ID import io.element.android.libraries.matrixtest.A_USER_NAME -import io.element.android.libraries.matrixtest.FakeMatrixClient import io.element.android.libraries.matrixtest.room.FakeMatrixRoom import io.element.android.libraries.textcomposer.MessageComposerMode import kotlinx.collections.immutable.persistentListOf @@ -134,14 +135,13 @@ class MessagesPresenterTest { private fun TestScope.createMessagePresenter( matrixRoom: MatrixRoom = FakeMatrixRoom() ): MessagesPresenter { - val matrixClient = FakeMatrixClient() val messageComposerPresenter = MessageComposerPresenter( appCoroutineScope = this, room = matrixRoom ) + val timelinePresenter = TimelinePresenter( - coroutineDispatchers = testCoroutineDispatchers(), - client = matrixClient, + timelineItemsFactory = aTimelineItemsFactory(), room = matrixRoom, ) val actionListPresenter = ActionListPresenter() @@ -154,25 +154,3 @@ class MessagesPresenterTest { } } -// TODO Move to common module to reuse -fun testCoroutineDispatchers() = CoroutineDispatchers( - io = UnconfinedTestDispatcher(), - computation = UnconfinedTestDispatcher(), - main = UnconfinedTestDispatcher(), - diffUpdateDispatcher = UnconfinedTestDispatcher(), -) - -// TODO Move to common module to reuse and remove this duplication -private fun aMessageEvent( - isMine: Boolean = true, - content: TimelineItemContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null), -) = TimelineItem.MessageEvent( - id = AN_EVENT_ID, - senderId = A_USER_ID.value, - senderDisplayName = A_USER_NAME, - senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME), - content = content, - sentTime = "", - isMine = isMine, - reactionsState = TimelineItemReactions(persistentListOf()) -) diff --git a/features/messages/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt b/features/messages/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt index 7da64088f5..2c0f9164a3 100644 --- a/features/messages/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt +++ b/features/messages/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt @@ -25,9 +25,9 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.actionlist.model.TimelineItemAction import io.element.android.features.messages.timeline.model.TimelineItem import io.element.android.features.messages.timeline.model.TimelineItemReactions -import io.element.android.features.messages.timeline.model.content.TimelineItemContent -import io.element.android.features.messages.timeline.model.content.TimelineItemRedactedContent -import io.element.android.features.messages.timeline.model.content.TimelineItemTextContent +import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent +import io.element.android.features.messages.timeline.model.event.TimelineItemRedactedContent +import io.element.android.features.messages.timeline.model.event.TimelineItemTextContent import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrixtest.AN_EVENT_ID import io.element.android.libraries.matrixtest.A_MESSAGE @@ -37,6 +37,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test +import org.matrix.rustcomponents.sdk.TimelineItemContent class ActionListPresenterTest { @Test @@ -163,9 +164,10 @@ class ActionListPresenterTest { private fun aMessageEvent( isMine: Boolean, - content: TimelineItemContent, -) = TimelineItem.MessageEvent( - id = AN_EVENT_ID, + content: TimelineItemEventContent, +) = TimelineItem.Event( + id = AN_EVENT_ID.value, + eventId = AN_EVENT_ID, senderId = A_USER_ID.value, senderDisplayName = A_USER_NAME, senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME), diff --git a/features/messages/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt b/features/messages/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt new file mode 100644 index 0000000000..9e665be409 --- /dev/null +++ b/features/messages/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt @@ -0,0 +1,43 @@ +/* + * 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.fixtures + +import io.element.android.features.messages.timeline.model.TimelineItem +import io.element.android.features.messages.timeline.model.TimelineItemReactions +import io.element.android.features.messages.timeline.model.event.TimelineItemEventContent +import io.element.android.features.messages.timeline.model.event.TimelineItemTextContent +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.matrixtest.AN_EVENT_ID +import io.element.android.libraries.matrixtest.A_MESSAGE +import io.element.android.libraries.matrixtest.A_USER_ID +import io.element.android.libraries.matrixtest.A_USER_NAME +import kotlinx.collections.immutable.persistentListOf + +internal fun aMessageEvent( + isMine: Boolean = true, + content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null), +) = TimelineItem.Event( + id = AN_EVENT_ID.value, + eventId = AN_EVENT_ID, + senderId = A_USER_ID.value, + senderDisplayName = A_USER_NAME, + senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME), + content = content, + sentTime = "", + isMine = isMine, + reactionsState = TimelineItemReactions(persistentListOf()) +) diff --git a/features/messages/src/test/kotlin/io/element/android/features/messages/fixtures/testCoroutineDispatchers.kt b/features/messages/src/test/kotlin/io/element/android/features/messages/fixtures/testCoroutineDispatchers.kt new file mode 100644 index 0000000000..60b0b7cf4b --- /dev/null +++ b/features/messages/src/test/kotlin/io/element/android/features/messages/fixtures/testCoroutineDispatchers.kt @@ -0,0 +1,28 @@ +/* + * 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.fixtures + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import kotlinx.coroutines.test.UnconfinedTestDispatcher + +// TODO Move to common module to reuse +internal fun testCoroutineDispatchers() = CoroutineDispatchers( + io = UnconfinedTestDispatcher(), + computation = UnconfinedTestDispatcher(), + main = UnconfinedTestDispatcher(), + diffUpdateDispatcher = UnconfinedTestDispatcher(), +) diff --git a/features/messages/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt b/features/messages/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt new file mode 100644 index 0000000000..d63c85eb0b --- /dev/null +++ b/features/messages/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt @@ -0,0 +1,52 @@ +/* + * 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.fixtures + +import io.element.android.features.messages.timeline.factories.TimelineItemsFactory +import io.element.android.features.messages.timeline.factories.event.TimelineItemContentFactory +import io.element.android.features.messages.timeline.factories.event.TimelineItemContentFailedToParseMessageFactory +import io.element.android.features.messages.timeline.factories.event.TimelineItemContentFailedToParseStateFactory +import io.element.android.features.messages.timeline.factories.event.TimelineItemContentMessageFactory +import io.element.android.features.messages.timeline.factories.event.TimelineItemContentProfileChangeFactory +import io.element.android.features.messages.timeline.factories.event.TimelineItemContentRedactedFactory +import io.element.android.features.messages.timeline.factories.event.TimelineItemContentRoomMembershipFactory +import io.element.android.features.messages.timeline.factories.event.TimelineItemContentStateFactory +import io.element.android.features.messages.timeline.factories.event.TimelineItemContentStickerFactory +import io.element.android.features.messages.timeline.factories.event.TimelineItemContentUTDFactory +import io.element.android.features.messages.timeline.factories.event.TimelineItemEventFactory +import io.element.android.features.messages.timeline.factories.virtual.TimelineItemDaySeparatorFactory +import io.element.android.features.messages.timeline.factories.virtual.TimelineItemVirtualFactory + +internal fun aTimelineItemsFactory() = TimelineItemsFactory( + dispatchers = testCoroutineDispatchers(), + eventItemFactory = TimelineItemEventFactory( + TimelineItemContentFactory( + messageFactory = TimelineItemContentMessageFactory(), + redactedMessageFactory = TimelineItemContentRedactedFactory(), + stickerFactory = TimelineItemContentStickerFactory(), + utdFactory = TimelineItemContentUTDFactory(), + roomMembershipFactory = TimelineItemContentRoomMembershipFactory(), + profileChangeFactory = TimelineItemContentProfileChangeFactory(), + stateFactory = TimelineItemContentStateFactory(), + failedToParseMessageFactory = TimelineItemContentFailedToParseMessageFactory(), + failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory() + ) + ), + virtualItemFactory = TimelineItemVirtualFactory( + daySeparatorFactory = TimelineItemDaySeparatorFactory(), + ) +) diff --git a/features/messages/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt b/features/messages/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt index ee73dc260b..12b5b649ed 100644 --- a/features/messages/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt +++ b/features/messages/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt @@ -22,10 +22,8 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.messages.timeline.model.TimelineItem -import io.element.android.libraries.matrix.timeline.MatrixTimelineItem +import io.element.android.features.messages.fixtures.aTimelineItemsFactory import io.element.android.libraries.matrixtest.AN_EVENT_ID -import io.element.android.libraries.matrixtest.FakeMatrixClient import io.element.android.libraries.matrixtest.room.FakeMatrixRoom import io.element.android.libraries.matrixtest.timeline.FakeMatrixTimeline import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -36,9 +34,8 @@ class TimelinePresenterTest { @Test fun `present - initial state`() = runTest { val presenter = TimelinePresenter( - testCoroutineDispatchers(), - FakeMatrixClient(), - FakeMatrixRoom() + timelineItemsFactory = aTimelineItemsFactory(), + room = FakeMatrixRoom(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -53,19 +50,19 @@ class TimelinePresenterTest { val matrixTimeline = FakeMatrixTimeline() val matrixRoom = FakeMatrixRoom(matrixTimeline = matrixTimeline) val presenter = TimelinePresenter( - testCoroutineDispatchers(), - FakeMatrixClient(), - matrixRoom + timelineItemsFactory = aTimelineItemsFactory(), + room = matrixRoom, ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.hasMoreToLoad).isTrue() - matrixTimeline.givenHasMoreToLoad(false) - initialState.eventSink.invoke(TimelineEvents.LoadMore) + assertThat(initialState.paginationState.canBackPaginate).isTrue() + matrixTimeline.updatePaginationState { + copy(canBackPaginate = false) + } val loadedState = awaitItem() - assertThat(loadedState.hasMoreToLoad).isFalse() + assertThat(loadedState.paginationState.canBackPaginate).isFalse() } } @@ -74,9 +71,8 @@ class TimelinePresenterTest { val matrixTimeline = FakeMatrixTimeline() val matrixRoom = FakeMatrixRoom(matrixTimeline = matrixTimeline) val presenter = TimelinePresenter( - testCoroutineDispatchers(), - FakeMatrixClient(), - matrixRoom + timelineItemsFactory = aTimelineItemsFactory(), + room = matrixRoom, ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -91,26 +87,4 @@ class TimelinePresenterTest { assertThat(withoutHighlightedState.highlightedEventId).isNull() } } - - @Test - fun `present - test callback`() = runTest { - val matrixTimeline = FakeMatrixTimeline() - val matrixRoom = FakeMatrixRoom(matrixTimeline = matrixTimeline) - val presenter = TimelinePresenter( - testCoroutineDispatchers(), - FakeMatrixClient(), - matrixRoom - ) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.timelineItems).isEmpty() - // Simulate callback from the SDK - matrixTimeline.callback?.onPushedTimelineItem(MatrixTimelineItem.Virtual) - val nonEmptyState = awaitItem() - assertThat(nonEmptyState.timelineItems).isNotEmpty() - assertThat(nonEmptyState.timelineItems[0]).isEqualTo(TimelineItem.Virtual("virtual_item_0")) - } - } } diff --git a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/timeline/FakeMatrixTimeline.kt b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/timeline/FakeMatrixTimeline.kt index 48ce246c03..bdf4c6dc16 100644 --- a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/timeline/FakeMatrixTimeline.kt +++ b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/timeline/FakeMatrixTimeline.kt @@ -31,6 +31,10 @@ class FakeMatrixTimeline : MatrixTimeline { MatrixTimeline.PaginationState(canBackPaginate = true, isBackPaginating = false) ) + fun updatePaginationState(update: (MatrixTimeline.PaginationState.() -> MatrixTimeline.PaginationState)) { + paginationState.value = update(paginationState.value) + } + override fun paginationState(): StateFlow { return paginationState }