Timeline permalink : start updating tests
This commit is contained in:
parent
bb0ba5c4bf
commit
60192bd080
19 changed files with 410 additions and 295 deletions
|
|
@ -288,7 +288,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
emoji: String,
|
emoji: String,
|
||||||
eventId: EventId,
|
eventId: EventId,
|
||||||
) = launch(dispatchers.io) {
|
) = launch(dispatchers.io) {
|
||||||
timelineController.invokeOnTimeline {
|
timelineController.invokeOnCurrentTimeline {
|
||||||
toggleReaction(emoji, eventId)
|
toggleReaction(emoji, eventId)
|
||||||
.onFailure { Timber.e(it) }
|
.onFailure { Timber.e(it) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import io.element.android.libraries.architecture.Presenter
|
||||||
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.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 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
|
||||||
|
|
@ -37,8 +38,8 @@ import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ForwardMessagesPresenter @AssistedInject constructor(
|
class ForwardMessagesPresenter @AssistedInject constructor(
|
||||||
@Assisted eventId: String,
|
@Assisted eventId: String,
|
||||||
private val room: MatrixRoom,
|
|
||||||
private val matrixCoroutineScope: CoroutineScope,
|
private val matrixCoroutineScope: CoroutineScope,
|
||||||
|
private val timelineProvider: TimelineProvider,
|
||||||
) : Presenter<ForwardMessagesState> {
|
) : Presenter<ForwardMessagesState> {
|
||||||
private val eventId: EventId = EventId(eventId)
|
private val eventId: EventId = EventId(eventId)
|
||||||
|
|
||||||
|
|
@ -79,7 +80,7 @@ class ForwardMessagesPresenter @AssistedInject constructor(
|
||||||
isForwardMessagesState: MutableState<AsyncData<ImmutableList<RoomId>>>,
|
isForwardMessagesState: MutableState<AsyncData<ImmutableList<RoomId>>>,
|
||||||
) = launch {
|
) = launch {
|
||||||
isForwardMessagesState.value = AsyncData.Loading()
|
isForwardMessagesState.value = AsyncData.Loading()
|
||||||
room.forwardEvent(eventId, roomIds).fold(
|
timelineProvider.getActiveTimeline().forwardEvent(eventId, roomIds).fold(
|
||||||
{ isForwardMessagesState.value = AsyncData.Success(roomIds) },
|
{ isForwardMessagesState.value = AsyncData.Success(roomIds) },
|
||||||
{ isForwardMessagesState.value = AsyncData.Failure(it) }
|
{ isForwardMessagesState.value = AsyncData.Failure(it) }
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -266,7 +266,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
is MessageComposerMode.Quote -> null
|
is MessageComposerMode.Quote -> null
|
||||||
}.let { relatedEventId ->
|
}.let { relatedEventId ->
|
||||||
appCoroutineScope.launch {
|
appCoroutineScope.launch {
|
||||||
timelineController.invokeOnTimeline {
|
timelineController.invokeOnCurrentTimeline {
|
||||||
enterSpecialMode(relatedEventId)
|
enterSpecialMode(relatedEventId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -390,14 +390,14 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
is MessageComposerMode.Edit -> {
|
is MessageComposerMode.Edit -> {
|
||||||
val eventId = capturedMode.eventId
|
val eventId = capturedMode.eventId
|
||||||
val transactionId = capturedMode.transactionId
|
val transactionId = capturedMode.transactionId
|
||||||
timelineController.invokeOnTimeline {
|
timelineController.invokeOnCurrentTimeline {
|
||||||
editMessage(eventId, transactionId, message.markdown, message.html, mentions)
|
editMessage(eventId, transactionId, message.markdown, message.html, mentions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is MessageComposerMode.Quote -> TODO()
|
is MessageComposerMode.Quote -> TODO()
|
||||||
is MessageComposerMode.Reply -> {
|
is MessageComposerMode.Reply -> {
|
||||||
timelineController.invokeOnTimeline {
|
timelineController.invokeOnCurrentTimeline {
|
||||||
replyMessage(capturedMode.eventId, message.markdown, message.html, mentions)
|
replyMessage(capturedMode.eventId, message.markdown, message.html, mentions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline
|
package io.element.android.features.messages.impl.timeline
|
||||||
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.di.SingleIn
|
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
|
||||||
|
|
@ -26,9 +24,7 @@ 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.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.ReceiptType
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -65,7 +61,7 @@ class TimelineController @Inject constructor(
|
||||||
return detachedTimeline.map { !it.isPresent }
|
return detachedTimeline.map { !it.isPresent }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun invokeOnTimeline(block: suspend (Timeline.() -> Any)) {
|
suspend fun invokeOnCurrentTimeline(block: suspend (Timeline.() -> Any)) {
|
||||||
currentTimelineFlow().first().run {
|
currentTimelineFlow().first().run {
|
||||||
block(this)
|
block(this)
|
||||||
}
|
}
|
||||||
|
|
@ -124,37 +120,6 @@ class TimelineController @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendReadReceiptIfNeeded(
|
|
||||||
firstVisibleIndex: Int,
|
|
||||||
timelineItems: ImmutableList<TimelineItem>,
|
|
||||||
lastReadReceiptId: MutableState<EventId?>,
|
|
||||||
readReceiptType: ReceiptType,
|
|
||||||
) {
|
|
||||||
// If we are at the bottom of timeline, we mark the room as read.
|
|
||||||
if (firstVisibleIndex == 0) {
|
|
||||||
room.markAsRead(receiptType = readReceiptType)
|
|
||||||
} else {
|
|
||||||
// Get last valid EventId seen by the user, as the first index might refer to a Virtual item
|
|
||||||
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
|
|
||||||
if (eventId != null && eventId != lastReadReceiptId.value) {
|
|
||||||
lastReadReceiptId.value = eventId
|
|
||||||
currentTimelineFlow()
|
|
||||||
.first()
|
|
||||||
.sendReadReceipt(eventId = eventId, receiptType = readReceiptType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getLastEventIdBeforeOrAt(index: Int, items: ImmutableList<TimelineItem>): EventId? {
|
|
||||||
for (i in index until items.count()) {
|
|
||||||
val item = items[i]
|
|
||||||
if (item is TimelineItem.Event) {
|
|
||||||
return item.eventId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getActiveTimeline(): Timeline {
|
override suspend fun getActiveTimeline(): Timeline {
|
||||||
return currentTimelineFlow().first()
|
return currentTimelineFlow().first()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
if (event.firstIndex == 0) {
|
if (event.firstIndex == 0) {
|
||||||
newEventState.value = NewEventState.None
|
newEventState.value = NewEventState.None
|
||||||
}
|
}
|
||||||
|
println("## sendReadReceiptIfNeeded firstVisibleIndex: ${event.firstIndex}")
|
||||||
appScope.sendReadReceiptIfNeeded(
|
appScope.sendReadReceiptIfNeeded(
|
||||||
firstVisibleIndex = event.firstIndex,
|
firstVisibleIndex = event.firstIndex,
|
||||||
timelineItems = timelineItems,
|
timelineItems = timelineItems,
|
||||||
|
|
@ -256,7 +257,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
|
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
|
||||||
if (eventId != null && eventId != lastReadReceiptId.value) {
|
if (eventId != null && eventId != lastReadReceiptId.value) {
|
||||||
lastReadReceiptId.value = eventId
|
lastReadReceiptId.value = eventId
|
||||||
//timeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType)
|
room.liveTimeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,10 +116,6 @@ fun TimelineView(
|
||||||
// TODO implement this logic once we have support to 'jump to event X' in sliding sync
|
// TODO implement this logic once we have support to 'jump to event X' in sliding sync
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(key1 = state.timelineItems) {
|
|
||||||
Timber.d("TimelineView - timelineItem identifiers: ${state.timelineItems.joinToString(", ") { it.identifier() }}")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms
|
// Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms
|
||||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
|
|
|
||||||
|
|
@ -62,10 +62,6 @@ class TimelineItemsFactory @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun items(): StateFlow<ImmutableList<TimelineItem>> {
|
|
||||||
return timelineItems
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun collectItemsAsState(): State<ImmutableList<TimelineItem>> {
|
fun collectItemsAsState(): State<ImmutableList<TimelineItem>> {
|
||||||
return timelineItems.collectAsState()
|
return timelineItems.collectAsState()
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||||
import io.element.android.features.messages.impl.messagesummary.FakeMessageSummaryFormatter
|
import io.element.android.features.messages.impl.messagesummary.FakeMessageSummaryFormatter
|
||||||
import io.element.android.features.messages.impl.textcomposer.TestRichTextEditorStateFactory
|
import io.element.android.features.messages.impl.textcomposer.TestRichTextEditorStateFactory
|
||||||
|
import io.element.android.features.messages.impl.timeline.TimelineController
|
||||||
|
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
|
||||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
|
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.FakeEmojibaseProvider
|
import io.element.android.features.messages.impl.timeline.components.customreaction.FakeEmojibaseProvider
|
||||||
|
|
@ -748,6 +750,7 @@ class MessagesPresenterTest {
|
||||||
currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
|
currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
|
||||||
permalinkParser = FakePermalinkParser(),
|
permalinkParser = FakePermalinkParser(),
|
||||||
permalinkBuilder = FakePermalinkBuilder(),
|
permalinkBuilder = FakePermalinkBuilder(),
|
||||||
|
timelineController = TimelineController(matrixRoom),
|
||||||
)
|
)
|
||||||
val voiceMessageComposerPresenter = VoiceMessageComposerPresenter(
|
val voiceMessageComposerPresenter = VoiceMessageComposerPresenter(
|
||||||
this,
|
this,
|
||||||
|
|
@ -768,6 +771,8 @@ class MessagesPresenterTest {
|
||||||
endPollAction = endPollAction,
|
endPollAction = endPollAction,
|
||||||
sendPollResponseAction = FakeSendPollResponseAction(),
|
sendPollResponseAction = FakeSendPollResponseAction(),
|
||||||
sessionPreferencesStore = sessionPreferencesStore,
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
|
timelineItemIndexer = TimelineItemIndexer(),
|
||||||
|
timelineController = TimelineController(matrixRoom),
|
||||||
)
|
)
|
||||||
val timelinePresenterFactory = object : TimelinePresenter.Factory {
|
val timelinePresenterFactory = object : TimelinePresenter.Factory {
|
||||||
override fun create(navigator: MessagesNavigator): TimelinePresenter {
|
override fun create(navigator: MessagesNavigator): TimelinePresenter {
|
||||||
|
|
@ -804,6 +809,7 @@ class MessagesPresenterTest {
|
||||||
buildMeta = aBuildMeta(),
|
buildMeta = aBuildMeta(),
|
||||||
dispatchers = coroutineDispatchers,
|
dispatchers = coroutineDispatchers,
|
||||||
htmlConverterProvider = FakeHtmlConverterProvider(),
|
htmlConverterProvider = FakeHtmlConverterProvider(),
|
||||||
|
timelineController = TimelineController(matrixRoom),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.fixtures
|
package io.element.android.features.messages.impl.fixtures
|
||||||
|
|
||||||
|
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
|
||||||
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.factories.event.TimelineItemContentFactory
|
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFactory
|
||||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFailedToParseMessageFactory
|
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFailedToParseMessageFactory
|
||||||
|
|
@ -46,7 +47,9 @@ import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorW
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
||||||
internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
internal fun TestScope.aTimelineItemsFactory(
|
||||||
|
timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer()
|
||||||
|
): TimelineItemsFactory {
|
||||||
val timelineEventFormatter = aTimelineEventFormatter()
|
val timelineEventFormatter = aTimelineEventFormatter()
|
||||||
val matrixClient = FakeMatrixClient()
|
val matrixClient = FakeMatrixClient()
|
||||||
return TimelineItemsFactory(
|
return TimelineItemsFactory(
|
||||||
|
|
@ -83,6 +86,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
timelineItemGrouper = TimelineItemGrouper(),
|
timelineItemGrouper = TimelineItemGrouper(),
|
||||||
|
timelineItemIndexer = timelineItemIndexer,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ 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.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
|
||||||
|
|
@ -91,7 +92,7 @@ class ForwardMessagesPresenterTests {
|
||||||
coroutineScope: CoroutineScope = this,
|
coroutineScope: CoroutineScope = this,
|
||||||
) = ForwardMessagesPresenter(
|
) = ForwardMessagesPresenter(
|
||||||
eventId = eventId.value,
|
eventId = eventId.value,
|
||||||
room = fakeMatrixRoom,
|
timelineProvider = LiveTimelineProvider(fakeMatrixRoom),
|
||||||
matrixCoroutineScope = coroutineScope,
|
matrixCoroutineScope = coroutineScope,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||||
|
import io.element.android.features.messages.impl.timeline.TimelineController
|
||||||
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
|
|
@ -968,6 +969,7 @@ class MessageComposerPresenterTest {
|
||||||
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter),
|
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter),
|
||||||
permalinkParser = FakePermalinkParser(),
|
permalinkParser = FakePermalinkParser(),
|
||||||
permalinkBuilder = permalinkBuilder,
|
permalinkBuilder = permalinkBuilder,
|
||||||
|
timelineController = TimelineController(room),
|
||||||
)
|
)
|
||||||
|
|
||||||
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,11 @@ 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.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
|
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
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.ReceiptType
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
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.ReactionSender
|
import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
|
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
|
||||||
|
|
@ -47,20 +49,28 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||||
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.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||||
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 io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
import io.element.android.libraries.matrix.ui.components.aMatrixUserList
|
||||||
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.awaitLastSequentialItem
|
||||||
import io.element.android.tests.testutils.awaitWithLatch
|
|
||||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||||
|
import io.element.android.tests.testutils.lambda.any
|
||||||
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.getAndUpdate
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.advanceTimeBy
|
||||||
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runCurrent
|
import kotlinx.coroutines.test.runCurrent
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -71,7 +81,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
|
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
|
||||||
private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
|
||||||
|
|
||||||
class TimelinePresenterTest {
|
@OptIn(ExperimentalCoroutinesApi::class) class TimelinePresenterTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val warmUpRule = WarmUpRule()
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
|
@ -83,58 +93,49 @@ class TimelinePresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
assertThat(initialState.timelineItems).isEmpty()
|
assertThat(initialState.timelineItems).isEmpty()
|
||||||
val loadedNoTimelineState = awaitItem()
|
assertThat(initialState.isLive).isTrue()
|
||||||
assertThat(loadedNoTimelineState.timelineItems).isEmpty()
|
assertThat(initialState.newEventState).isEqualTo(NewEventState.None)
|
||||||
|
assertThat(initialState.focusedEventId).isNull()
|
||||||
|
assertThat(initialState.focusRequestState).isEqualTo(FocusRequestState.None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - load more`() = runTest {
|
fun `present - load more`() = runTest {
|
||||||
val presenter = createTimelinePresenter()
|
val paginateLambda = lambdaRecorder { _: Timeline.PaginationDirection ->
|
||||||
|
Result.success(false)
|
||||||
|
}
|
||||||
|
val timeline = FakeTimeline().apply {
|
||||||
|
this.paginateLambda = paginateLambda
|
||||||
|
}
|
||||||
|
val presenter = createTimelinePresenter(timeline = timeline)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.backPaginationStatus.hasMoreToLoadBackwards).isTrue()
|
initialState.eventSink.invoke(TimelineEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS))
|
||||||
assertThat(initialState.backPaginationStatus.isBackPaginating).isFalse()
|
initialState.eventSink.invoke(TimelineEvents.LoadMore(Timeline.PaginationDirection.FORWARDS))
|
||||||
initialState.eventSink.invoke(TimelineEvents.LoadMore)
|
assert(paginateLambda)
|
||||||
val inPaginationState = awaitItem()
|
.isCalledExactly(2)
|
||||||
assertThat(inPaginationState.backPaginationStatus.isBackPaginating).isTrue()
|
.withSequence(
|
||||||
assertThat(inPaginationState.backPaginationStatus.hasMoreToLoadBackwards).isTrue()
|
listOf(value(Timeline.PaginationDirection.BACKWARDS)),
|
||||||
val postPaginationState = awaitItem()
|
listOf(value(Timeline.PaginationDirection.FORWARDS))
|
||||||
assertThat(postPaginationState.backPaginationStatus.hasMoreToLoadBackwards).isTrue()
|
)
|
||||||
assertThat(postPaginationState.backPaginationStatus.isBackPaginating).isFalse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - set highlighted event`() = runTest {
|
|
||||||
val presenter = createTimelinePresenter()
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitFirstItem()
|
|
||||||
skipItems(1)
|
|
||||||
assertThat(initialState.highlightedEventId).isNull()
|
|
||||||
initialState.eventSink.invoke(TimelineEvents.SetHighlightedEvent(AN_EVENT_ID))
|
|
||||||
val withHighlightedState = awaitItem()
|
|
||||||
assertThat(withHighlightedState.highlightedEventId).isEqualTo(AN_EVENT_ID)
|
|
||||||
initialState.eventSink.invoke(TimelineEvents.SetHighlightedEvent(null))
|
|
||||||
val withoutHighlightedState = awaitItem()
|
|
||||||
assertThat(withoutHighlightedState.highlightedEventId).isNull()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun `present - on scroll finished mark a room as read if the first visible index is 0`() = runTest(StandardTestDispatcher()) {
|
fun `present - on scroll finished mark a room as read if the first visible index is 0`() = runTest(StandardTestDispatcher()) {
|
||||||
val timeline = FakeMatrixTimeline(
|
val timeline = FakeTimeline(
|
||||||
initialTimelineItems = listOf(
|
timelineItems = flowOf(
|
||||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
|
listOf(
|
||||||
|
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
val room = FakeMatrixRoom(liveTimeline = timeline)
|
||||||
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false)
|
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false)
|
||||||
val room = FakeMatrixRoom(matrixTimeline = timeline)
|
|
||||||
val presenter = createTimelinePresenter(
|
val presenter = createTimelinePresenter(
|
||||||
timeline = timeline,
|
timeline = timeline,
|
||||||
room = room,
|
room = room,
|
||||||
|
|
@ -143,7 +144,6 @@ class TimelinePresenterTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
assertThat(timeline.sentReadReceipts).isEmpty()
|
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
||||||
runCurrent()
|
runCurrent()
|
||||||
|
|
@ -154,48 +154,62 @@ class TimelinePresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
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 sendReadReceiptsLambda = lambdaRecorder { _: EventId, _: ReceiptType ->
|
||||||
initialTimelineItems = listOf(
|
Result.success(Unit)
|
||||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
|
}
|
||||||
MatrixTimelineItem.Event(
|
val timeline = FakeTimeline(
|
||||||
uniqueId = FAKE_UNIQUE_ID_2,
|
timelineItems = flowOf(
|
||||||
event = anEventTimelineItem(
|
listOf(
|
||||||
eventId = AN_EVENT_ID_2,
|
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
|
||||||
content = aMessageContent("Test message")
|
MatrixTimelineItem.Event(
|
||||||
|
uniqueId = FAKE_UNIQUE_ID_2,
|
||||||
|
event = anEventTimelineItem(
|
||||||
|
eventId = AN_EVENT_ID_2,
|
||||||
|
content = aMessageContent("Test message")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
).apply {
|
||||||
|
this.sendReadReceiptLambda = sendReadReceiptsLambda
|
||||||
|
}
|
||||||
val presenter = createTimelinePresenter(timeline)
|
val presenter = createTimelinePresenter(timeline)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
assertThat(timeline.sentReadReceipts).isEmpty()
|
skipItems(1)
|
||||||
val initialState = awaitFirstItem()
|
awaitItem().run {
|
||||||
awaitWithLatch { latch ->
|
eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
||||||
timeline.sendReadReceiptLatch = latch
|
|
||||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
|
||||||
}
|
}
|
||||||
assertThat(timeline.sentReadReceipts).isNotEmpty()
|
advanceUntilIdle()
|
||||||
assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ)
|
assert(sendReadReceiptsLambda)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(any(), value(ReceiptType.READ))
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - on scroll finished send a private read receipt if an event is at an index other than 0 and public read receipts are disabled`() = runTest {
|
fun `present - on scroll finished send a private read receipt if an event is at an index other than 0 and public read receipts are disabled`() = runTest {
|
||||||
val timeline = FakeMatrixTimeline(
|
val sendReadReceiptsLambda = lambdaRecorder { _: EventId, _: ReceiptType ->
|
||||||
initialTimelineItems = listOf(
|
Result.success(Unit)
|
||||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
|
}
|
||||||
MatrixTimelineItem.Event(
|
val timeline = FakeTimeline(
|
||||||
uniqueId = FAKE_UNIQUE_ID_2,
|
timelineItems = flowOf(
|
||||||
event = anEventTimelineItem(
|
listOf(
|
||||||
eventId = AN_EVENT_ID_2,
|
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
|
||||||
content = aMessageContent("Test message")
|
MatrixTimelineItem.Event(
|
||||||
|
uniqueId = FAKE_UNIQUE_ID_2,
|
||||||
|
event = anEventTimelineItem(
|
||||||
|
eventId = AN_EVENT_ID_2,
|
||||||
|
content = aMessageContent("Test message")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
).apply {
|
||||||
|
this.sendReadReceiptLambda = sendReadReceiptsLambda
|
||||||
|
}
|
||||||
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false)
|
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false)
|
||||||
val presenter = createTimelinePresenter(
|
val presenter = createTimelinePresenter(
|
||||||
timeline = timeline,
|
timeline = timeline,
|
||||||
|
|
@ -204,75 +218,86 @@ class TimelinePresenterTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
assertThat(timeline.sentReadReceipts).isEmpty()
|
skipItems(1)
|
||||||
val initialState = awaitFirstItem()
|
awaitItem().run {
|
||||||
awaitWithLatch { latch ->
|
eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
||||||
timeline.sendReadReceiptLatch = latch
|
eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
||||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
|
|
||||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
|
||||||
}
|
}
|
||||||
assertThat(timeline.sentReadReceipts).isNotEmpty()
|
advanceUntilIdle()
|
||||||
assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ_PRIVATE)
|
assert(sendReadReceiptsLambda)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(any(), value(ReceiptType.READ_PRIVATE))
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - on scroll finished will not send read receipt the first visible event is the same as before`() = runTest {
|
fun `present - on scroll finished will not send read receipt the first visible event is the same as before`() = runTest {
|
||||||
val timeline = FakeMatrixTimeline(
|
val sendReadReceiptsLambda = lambdaRecorder { _: EventId, _: ReceiptType ->
|
||||||
initialTimelineItems = listOf(
|
Result.success(Unit)
|
||||||
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
|
}
|
||||||
MatrixTimelineItem.Event(
|
val timeline = FakeTimeline(
|
||||||
uniqueId = FAKE_UNIQUE_ID_2,
|
timelineItems = flowOf(
|
||||||
event = anEventTimelineItem(
|
listOf(
|
||||||
eventId = AN_EVENT_ID_2,
|
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
|
||||||
content = aMessageContent("Test message")
|
MatrixTimelineItem.Event(
|
||||||
|
uniqueId = FAKE_UNIQUE_ID_2,
|
||||||
|
event = anEventTimelineItem(
|
||||||
|
eventId = AN_EVENT_ID_2,
|
||||||
|
content = aMessageContent("Test message")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
).apply {
|
||||||
|
this.sendReadReceiptLambda = sendReadReceiptsLambda
|
||||||
|
}
|
||||||
val presenter = createTimelinePresenter(timeline)
|
val presenter = createTimelinePresenter(timeline)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
assertThat(timeline.sentReadReceipts).isEmpty()
|
skipItems(1)
|
||||||
val initialState = awaitFirstItem()
|
awaitItem().run {
|
||||||
awaitWithLatch { latch ->
|
eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
||||||
timeline.sendReadReceiptLatch = latch
|
eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
||||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
|
||||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
|
||||||
}
|
}
|
||||||
assertThat(timeline.sentReadReceipts).hasSize(1)
|
advanceUntilIdle()
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
assert(sendReadReceiptsLambda).isCalledOnce()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
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 sendReadReceiptsLambda = lambdaRecorder { _: EventId, _: ReceiptType ->
|
||||||
initialTimelineItems = listOf(
|
Result.success(Unit)
|
||||||
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker),
|
}
|
||||||
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker)
|
val timeline = FakeTimeline(
|
||||||
|
timelineItems = flowOf(
|
||||||
|
listOf(
|
||||||
|
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker),
|
||||||
|
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
).apply {
|
||||||
|
this.sendReadReceiptLambda = sendReadReceiptsLambda
|
||||||
|
}
|
||||||
val presenter = createTimelinePresenter(timeline)
|
val presenter = createTimelinePresenter(timeline)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
assertThat(timeline.sentReadReceipts).isEmpty()
|
skipItems(1)
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
awaitWithLatch { latch ->
|
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
||||||
timeline.sendReadReceiptLatch = latch
|
|
||||||
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
|
|
||||||
}
|
|
||||||
assertThat(timeline.sentReadReceipts).isEmpty()
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
assert(sendReadReceiptsLambda).isNeverCalled()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - covers newEventState scenarios`() = runTest {
|
fun `present - covers newEventState scenarios`() = runTest {
|
||||||
val timeline = FakeMatrixTimeline()
|
val timelineItems = MutableStateFlow(emptyList<MatrixTimelineItem>())
|
||||||
|
val timeline = FakeTimeline(timelineItems = timelineItems)
|
||||||
val presenter = createTimelinePresenter(timeline)
|
val presenter = createTimelinePresenter(timeline)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -280,12 +305,12 @@ class TimelinePresenterTest {
|
||||||
val initialState = awaitFirstItem()
|
val initialState = awaitFirstItem()
|
||||||
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 {
|
timelineItems.emit(
|
||||||
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 ->
|
timelineItems.getAndUpdate { 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))
|
||||||
}
|
}
|
||||||
|
|
@ -294,7 +319,7 @@ class TimelinePresenterTest {
|
||||||
assertThat(state.newEventState).isEqualTo(NewEventState.FromMe)
|
assertThat(state.newEventState).isEqualTo(NewEventState.FromMe)
|
||||||
}
|
}
|
||||||
// Mimics receiving a message without clearing the previous FromMe
|
// Mimics receiving a message without clearing the previous FromMe
|
||||||
timeline.updateTimelineItems { items ->
|
timelineItems.getAndUpdate { items ->
|
||||||
val event = anEventTimelineItem(content = aMessageContent())
|
val event = anEventTimelineItem(content = aMessageContent())
|
||||||
items + listOf(MatrixTimelineItem.Event("2", event))
|
items + listOf(MatrixTimelineItem.Event("2", event))
|
||||||
}
|
}
|
||||||
|
|
@ -306,7 +331,7 @@ class TimelinePresenterTest {
|
||||||
assertThat(state.newEventState).isEqualTo(NewEventState.None)
|
assertThat(state.newEventState).isEqualTo(NewEventState.None)
|
||||||
}
|
}
|
||||||
// Mimics receiving a message and assert newEventState is FromOther
|
// Mimics receiving a message and assert newEventState is FromOther
|
||||||
timeline.updateTimelineItems { items ->
|
timelineItems.getAndUpdate { items ->
|
||||||
val event = anEventTimelineItem(content = aMessageContent())
|
val event = anEventTimelineItem(content = aMessageContent())
|
||||||
items + listOf(MatrixTimelineItem.Event("3", event))
|
items + listOf(MatrixTimelineItem.Event("3", event))
|
||||||
}
|
}
|
||||||
|
|
@ -320,7 +345,10 @@ class TimelinePresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - reaction ordering`() = runTest {
|
fun `present - reaction ordering`() = runTest {
|
||||||
val timeline = FakeMatrixTimeline()
|
val timelineItems = MutableStateFlow(emptyList<MatrixTimelineItem>())
|
||||||
|
val timeline = FakeTimeline(
|
||||||
|
timelineItems = timelineItems,
|
||||||
|
)
|
||||||
val presenter = createTimelinePresenter(timeline)
|
val presenter = createTimelinePresenter(timeline)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -348,10 +376,9 @@ class TimelinePresenterTest {
|
||||||
senders = persistentListOf(charlie)
|
senders = persistentListOf(charlie)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
timeline.updateTimelineItems {
|
timelineItems.emit(
|
||||||
listOf(MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(reactions = oneReaction)))
|
listOf(MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(reactions = oneReaction)))
|
||||||
}
|
)
|
||||||
skipItems(1)
|
|
||||||
val item = awaitItem().timelineItems.first()
|
val item = awaitItem().timelineItems.first()
|
||||||
assertThat(item).isInstanceOf(TimelineItem.Event::class.java)
|
assertThat(item).isInstanceOf(TimelineItem.Event::class.java)
|
||||||
val event = item as TimelineItem.Event
|
val event = item as TimelineItem.Event
|
||||||
|
|
@ -423,8 +450,10 @@ class TimelinePresenterTest {
|
||||||
fun `present - side effect on redacted items is invoked`() = runTest {
|
fun `present - side effect on redacted items is invoked`() = runTest {
|
||||||
val redactedVoiceMessageManager = FakeRedactedVoiceMessageManager()
|
val redactedVoiceMessageManager = FakeRedactedVoiceMessageManager()
|
||||||
val presenter = createTimelinePresenter(
|
val presenter = createTimelinePresenter(
|
||||||
timeline = FakeMatrixTimeline(
|
timeline = FakeTimeline(
|
||||||
initialTimelineItems = aRedactedMatrixTimeline(AN_EVENT_ID),
|
timelineItems = flowOf(
|
||||||
|
aRedactedMatrixTimeline(AN_EVENT_ID),
|
||||||
|
)
|
||||||
),
|
),
|
||||||
redactedVoiceMessageManager = redactedVoiceMessageManager,
|
redactedVoiceMessageManager = redactedVoiceMessageManager,
|
||||||
)
|
)
|
||||||
|
|
@ -432,32 +461,32 @@ class TimelinePresenterTest {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(0)
|
assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(0)
|
||||||
awaitFirstItem().let {
|
skipItems(2)
|
||||||
assertThat(it.timelineItems).isNotEmpty()
|
|
||||||
}
|
|
||||||
assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(1)
|
assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - when room member info is loaded, read receipts info should be updated`() = runTest {
|
fun `present - when room member info is loaded, read receipts info should be updated`() = runTest {
|
||||||
val timeline = FakeMatrixTimeline(
|
val timeline = FakeTimeline(
|
||||||
listOf(
|
timelineItems = flowOf(
|
||||||
MatrixTimelineItem.Event(
|
listOf(
|
||||||
FAKE_UNIQUE_ID,
|
MatrixTimelineItem.Event(
|
||||||
anEventTimelineItem(
|
FAKE_UNIQUE_ID,
|
||||||
sender = A_USER_ID,
|
anEventTimelineItem(
|
||||||
receipts = persistentListOf(
|
sender = A_USER_ID,
|
||||||
Receipt(
|
receipts = persistentListOf(
|
||||||
userId = A_USER_ID,
|
Receipt(
|
||||||
timestamp = 0L,
|
userId = A_USER_ID,
|
||||||
|
timestamp = 0L,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val room = FakeMatrixRoom(matrixTimeline = timeline).apply {
|
val room = FakeMatrixRoom(liveTimeline = timeline).apply {
|
||||||
givenRoomMembersState(MatrixRoomMembersState.Unknown)
|
givenRoomMembersState(MatrixRoomMembersState.Unknown)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -484,16 +513,12 @@ class TimelinePresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
|
||||||
// Skip 1 item if Mentions feature is enabled
|
|
||||||
if (FeatureFlags.Mentions.defaultValue) {
|
|
||||||
skipItems(1)
|
|
||||||
}
|
|
||||||
return awaitItem()
|
return awaitItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TestScope.createTimelinePresenter(
|
private fun TestScope.createTimelinePresenter(
|
||||||
timeline: MatrixTimeline = FakeMatrixTimeline(),
|
timeline: Timeline = FakeTimeline(),
|
||||||
room: FakeMatrixRoom = FakeMatrixRoom(matrixTimeline = timeline),
|
room: FakeMatrixRoom = FakeMatrixRoom(liveTimeline = timeline),
|
||||||
timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory(),
|
timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory(),
|
||||||
redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
|
||||||
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
|
||||||
|
|
@ -511,6 +536,8 @@ class TimelinePresenterTest {
|
||||||
endPollAction = endPollAction,
|
endPollAction = endPollAction,
|
||||||
sendPollResponseAction = sendPollResponseAction,
|
sendPollResponseAction = sendPollResponseAction,
|
||||||
sessionPreferencesStore = sessionPreferencesStore,
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
|
timelineItemIndexer = TimelineItemIndexer(),
|
||||||
|
timelineController = TimelineController(room),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,14 @@ package io.element.android.features.messages.impl.timeline
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel
|
||||||
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams
|
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
@ -37,10 +41,13 @@ class TimelineViewTest {
|
||||||
rule.setContent {
|
rule.setContent {
|
||||||
TimelineView(
|
TimelineView(
|
||||||
aTimelineState(
|
aTimelineState(
|
||||||
|
timelineItems = persistentListOf<TimelineItem>(
|
||||||
|
TimelineItem.Virtual(
|
||||||
|
id = "backward_pagination",
|
||||||
|
model = TimelineItemLoadingIndicatorModel(Timeline.PaginationDirection.BACKWARDS, 0)
|
||||||
|
),
|
||||||
|
),
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
paginationState = aPaginationState(
|
|
||||||
hasMoreToLoadBackwards = true,
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
typingNotificationState = aTypingNotificationState(),
|
typingNotificationState = aTypingNotificationState(),
|
||||||
onUserDataClicked = EnsureNeverCalledWithParam(),
|
onUserDataClicked = EnsureNeverCalledWithParam(),
|
||||||
|
|
@ -55,7 +62,7 @@ class TimelineViewTest {
|
||||||
onReadReceiptClick = EnsureNeverCalledWithParam(),
|
onReadReceiptClick = EnsureNeverCalledWithParam(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
eventsRecorder.assertSingle(TimelineEvents.LoadMore)
|
eventsRecorder.assertSingle(TimelineEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -65,9 +72,6 @@ class TimelineViewTest {
|
||||||
TimelineView(
|
TimelineView(
|
||||||
aTimelineState(
|
aTimelineState(
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
paginationState = aPaginationState(
|
|
||||||
hasMoreToLoadBackwards = false,
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
typingNotificationState = aTypingNotificationState(),
|
typingNotificationState = aTypingNotificationState(),
|
||||||
onUserDataClicked = EnsureNeverCalledWithParam(),
|
onUserDataClicked = EnsureNeverCalledWithParam(),
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,15 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
import io.element.android.libraries.matrix.api.poll.PollAnswer
|
||||||
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.PollContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
|
||||||
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||||
import io.element.android.libraries.matrix.test.timeline.aPollContent
|
import io.element.android.libraries.matrix.test.timeline.aPollContent
|
||||||
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
fun aPollTimeline(
|
fun aPollTimeline(
|
||||||
polls: Map<EventId, PollContent> = emptyMap(),
|
polls: Map<EventId, PollContent> = emptyMap(),
|
||||||
): FakeMatrixTimeline {
|
): FakeTimeline {
|
||||||
return FakeMatrixTimeline(
|
return FakeTimeline(
|
||||||
initialTimelineItems = polls.map { entry ->
|
initialTimelineItems = polls.map { entry ->
|
||||||
MatrixTimelineItem.Event(
|
MatrixTimelineItem.Event(
|
||||||
entry.key.value,
|
entry.key.value,
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ class CreatePollPresenterTest {
|
||||||
private var navUpInvocationsCount = 0
|
private var navUpInvocationsCount = 0
|
||||||
private val existingPoll = anOngoingPollContent()
|
private val existingPoll = anOngoingPollContent()
|
||||||
private val fakeMatrixRoom = FakeMatrixRoom(
|
private val fakeMatrixRoom = FakeMatrixRoom(
|
||||||
matrixTimeline = aPollTimeline(
|
liveTimeline = aPollTimeline(
|
||||||
mapOf(pollEventId to existingPoll)
|
mapOf(pollEventId to existingPoll)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -80,7 +80,7 @@ class CreatePollPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `in edit mode, if poll doesn't exist, error is tracked and screen is closed`() = runTest {
|
fun `in edit mode, if poll doesn't exist, error is tracked and screen is closed`() = runTest {
|
||||||
val room = FakeMatrixRoom(
|
val room = FakeMatrixRoom(
|
||||||
matrixTimeline = aPollTimeline()
|
liveTimeline = aPollTimeline()
|
||||||
)
|
)
|
||||||
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(AN_EVENT_ID), room = room)
|
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(AN_EVENT_ID), room = room)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ class PollHistoryPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
private val room = FakeMatrixRoom(
|
private val room = FakeMatrixRoom(
|
||||||
matrixTimeline = timeline
|
liveTimeline = timeline
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
|
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
|
||||||
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
|
||||||
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
|
||||||
|
|
@ -53,7 +54,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
||||||
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
|
||||||
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||||
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
|
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
import kotlinx.collections.immutable.ImmutableMap
|
import kotlinx.collections.immutable.ImmutableMap
|
||||||
|
|
@ -83,7 +84,7 @@ class FakeMatrixRoom(
|
||||||
override val joinedMemberCount: Long = 123L,
|
override val joinedMemberCount: Long = 123L,
|
||||||
override val activeMemberCount: Long = 234L,
|
override val activeMemberCount: Long = 234L,
|
||||||
val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
|
val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
|
||||||
private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(),
|
override val liveTimeline: Timeline = FakeTimeline(),
|
||||||
private var roomPermalinkResult: () -> Result<String> = { Result.success("room link") },
|
private var roomPermalinkResult: () -> Result<String> = { Result.success("room link") },
|
||||||
private var eventPermalinkResult: (EventId) -> Result<String> = { Result.success("event link") },
|
private var eventPermalinkResult: (EventId) -> Result<String> = { Result.success("event link") },
|
||||||
canRedactOwn: Boolean = false,
|
canRedactOwn: Boolean = false,
|
||||||
|
|
@ -214,7 +215,9 @@ class FakeMatrixRoom(
|
||||||
|
|
||||||
override val syncUpdateFlow: StateFlow<Long> = MutableStateFlow(0L)
|
override val syncUpdateFlow: StateFlow<Long> = MutableStateFlow(0L)
|
||||||
|
|
||||||
override val timeline: MatrixTimeline = matrixTimeline
|
override suspend fun timelineFocusedOnEvent(eventId: EventId): Timeline {
|
||||||
|
return FakeTimeline()
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun subscribeToSync() = Unit
|
override suspend fun subscribeToSync() = Unit
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.libraries.matrix.test.timeline
|
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.getAndUpdate
|
|
||||||
|
|
||||||
class FakeMatrixTimeline(
|
|
||||||
initialTimelineItems: List<MatrixTimelineItem> = emptyList(),
|
|
||||||
initialPaginationState: MatrixTimeline.PaginationState = MatrixTimeline.PaginationState(
|
|
||||||
hasMoreToLoadBackwards = true,
|
|
||||||
isBackPaginating = false,
|
|
||||||
beginningOfRoomReached = false,
|
|
||||||
)
|
|
||||||
) : MatrixTimeline {
|
|
||||||
private val _paginationState: MutableStateFlow<MatrixTimeline.PaginationState> = MutableStateFlow(initialPaginationState)
|
|
||||||
private val _timelineItems: MutableStateFlow<List<MatrixTimelineItem>> = MutableStateFlow(initialTimelineItems)
|
|
||||||
|
|
||||||
var sentReadReceipts = mutableListOf<Pair<EventId, ReceiptType>>()
|
|
||||||
private set
|
|
||||||
|
|
||||||
var sendReadReceiptLatch: CompletableDeferred<Unit>? = null
|
|
||||||
|
|
||||||
fun updatePaginationState(update: (MatrixTimeline.PaginationState.() -> MatrixTimeline.PaginationState)) {
|
|
||||||
_paginationState.getAndUpdate(update)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateTimelineItems(update: (items: List<MatrixTimelineItem>) -> List<MatrixTimelineItem>) {
|
|
||||||
_timelineItems.getAndUpdate(update)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val paginationState: StateFlow<MatrixTimeline.PaginationState> = _paginationState
|
|
||||||
|
|
||||||
override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems
|
|
||||||
|
|
||||||
override suspend fun paginateBackwards(requestSize: Int) = paginateBackwards()
|
|
||||||
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int) = paginateBackwards()
|
|
||||||
|
|
||||||
override val membershipChangeEventReceived = MutableSharedFlow<Unit>()
|
|
||||||
|
|
||||||
private suspend fun paginateBackwards(): Result<Unit> {
|
|
||||||
updatePaginationState {
|
|
||||||
copy(isBackPaginating = true)
|
|
||||||
}
|
|
||||||
delay(100)
|
|
||||||
updatePaginationState {
|
|
||||||
copy(isBackPaginating = false)
|
|
||||||
}
|
|
||||||
updateTimelineItems { timelineItems ->
|
|
||||||
timelineItems
|
|
||||||
}
|
|
||||||
return Result.success(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun givenMembershipChangeEventReceived() {
|
|
||||||
membershipChangeEventReceived.tryEmit(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> = simulateLongTask {
|
|
||||||
Result.success(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun sendReadReceipt(
|
|
||||||
eventId: EventId,
|
|
||||||
receiptType: ReceiptType,
|
|
||||||
): Result<Unit> = simulateLongTask {
|
|
||||||
sentReadReceipts.add(eventId to receiptType)
|
|
||||||
sendReadReceiptLatch?.complete(Unit)
|
|
||||||
Result.success(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() = Unit
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,205 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.matrix.test.timeline
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.TransactionId
|
||||||
|
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||||
|
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||||
|
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
|
||||||
|
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||||
|
import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
|
import io.element.android.libraries.matrix.api.room.Mention
|
||||||
|
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
|
||||||
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class FakeTimeline(
|
||||||
|
override val timelineItems: Flow<List<MatrixTimelineItem>> = MutableStateFlow(emptyList()),
|
||||||
|
private val backwardPaginationStatus: MutableStateFlow<Timeline.PaginationStatus> = MutableStateFlow(
|
||||||
|
Timeline.PaginationStatus(
|
||||||
|
isPaginating = false,
|
||||||
|
hasMoreToLoad = true
|
||||||
|
)
|
||||||
|
),
|
||||||
|
private val forwardPaginationStatus: MutableStateFlow<Timeline.PaginationStatus> = MutableStateFlow(
|
||||||
|
Timeline.PaginationStatus(
|
||||||
|
isPaginating = false,
|
||||||
|
hasMoreToLoad = false
|
||||||
|
)
|
||||||
|
),
|
||||||
|
override val membershipChangeEventReceived: Flow<Unit> = MutableSharedFlow(),
|
||||||
|
) : Timeline {
|
||||||
|
|
||||||
|
var sendMessageLambda: (body: String, htmlBody: String?, mentions: List<Mention>) -> Result<Unit> = { _, _, _ -> Result.success(Unit) }
|
||||||
|
override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit> = sendMessageLambda(body, htmlBody, mentions)
|
||||||
|
|
||||||
|
var editMessageLambda: (originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?, mentions: List<Mention>) -> Result<Unit> =
|
||||||
|
{ _, _, _, _, _ -> Result.success(Unit) }
|
||||||
|
|
||||||
|
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit> = editMessageLambda(
|
||||||
|
originalEventId,
|
||||||
|
transactionId,
|
||||||
|
body,
|
||||||
|
htmlBody,
|
||||||
|
mentions
|
||||||
|
)
|
||||||
|
|
||||||
|
var enterSpecialModeLambda: (eventId: EventId?) -> Result<Unit> = { Result.success(Unit) }
|
||||||
|
override suspend fun enterSpecialMode(eventId: EventId?): Result<Unit> = enterSpecialModeLambda(eventId)
|
||||||
|
|
||||||
|
var replyMessageLambda: (eventId: EventId, body: String, htmlBody: String?, mentions: List<Mention>) -> Result<Unit> =
|
||||||
|
{ _, _, _, _ -> Result.success(Unit) }
|
||||||
|
|
||||||
|
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit> = replyMessageLambda(
|
||||||
|
eventId,
|
||||||
|
body,
|
||||||
|
htmlBody,
|
||||||
|
mentions
|
||||||
|
)
|
||||||
|
|
||||||
|
var sendImageLambda: (file: File, thumbnailFile: File?, imageInfo: ImageInfo, body: String?, formattedBody: String?, progressCallback: ProgressCallback?) -> Result<MediaUploadHandler> =
|
||||||
|
{ _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) }
|
||||||
|
|
||||||
|
override suspend fun sendImage(file: File, thumbnailFile: File?, imageInfo: ImageInfo, body: String?, formattedBody: String?, progressCallback: ProgressCallback?): Result<MediaUploadHandler> = sendImageLambda(
|
||||||
|
file,
|
||||||
|
thumbnailFile,
|
||||||
|
imageInfo,
|
||||||
|
body,
|
||||||
|
formattedBody,
|
||||||
|
progressCallback
|
||||||
|
)
|
||||||
|
|
||||||
|
var sendVideoLambda: (file: File, thumbnailFile: File?, videoInfo: VideoInfo, body: String?, formattedBody: String?, progressCallback: ProgressCallback?) -> Result<MediaUploadHandler> =
|
||||||
|
{ _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) }
|
||||||
|
|
||||||
|
override suspend fun sendVideo(file: File, thumbnailFile: File?, videoInfo: VideoInfo, body: String?, formattedBody: String?, progressCallback: ProgressCallback?): Result<MediaUploadHandler> = sendVideoLambda(
|
||||||
|
file,
|
||||||
|
thumbnailFile,
|
||||||
|
videoInfo,
|
||||||
|
body,
|
||||||
|
formattedBody,
|
||||||
|
progressCallback
|
||||||
|
)
|
||||||
|
|
||||||
|
var sendAudioLambda: (file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?) -> Result<MediaUploadHandler> =
|
||||||
|
{ _, _, _ -> Result.success(FakeMediaUploadHandler()) }
|
||||||
|
|
||||||
|
override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler> = sendAudioLambda(
|
||||||
|
file,
|
||||||
|
audioInfo,
|
||||||
|
progressCallback
|
||||||
|
)
|
||||||
|
|
||||||
|
var sendFileLambda: (file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?) -> Result<MediaUploadHandler> =
|
||||||
|
{ _, _, _ -> Result.success(FakeMediaUploadHandler()) }
|
||||||
|
|
||||||
|
override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler> = sendFileLambda(
|
||||||
|
file,
|
||||||
|
fileInfo,
|
||||||
|
progressCallback
|
||||||
|
)
|
||||||
|
|
||||||
|
var toggleReactionLambda: (emoji: String, eventId: EventId) -> Result<Unit> = { _, _ -> Result.success(Unit) }
|
||||||
|
override suspend fun toggleReaction(emoji: String, eventId: EventId): Result<Unit> = toggleReactionLambda(emoji, eventId)
|
||||||
|
|
||||||
|
var forwardEventLambda: (eventId: EventId, roomIds: List<RoomId>) -> Result<Unit> = { _, _ -> Result.success(Unit) }
|
||||||
|
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = forwardEventLambda(eventId, roomIds)
|
||||||
|
|
||||||
|
var retrySendMessageLambda: (transactionId: TransactionId) -> Result<Unit> = { Result.success(Unit) }
|
||||||
|
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> = retrySendMessageLambda(transactionId)
|
||||||
|
|
||||||
|
var cancelSendLambda: (transactionId: TransactionId) -> Result<Unit> = { Result.success(Unit) }
|
||||||
|
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> = cancelSendLambda(transactionId)
|
||||||
|
|
||||||
|
var sendLocationLambda: (body: String, geoUri: String, description: String?, zoomLevel: Int?, assetType: AssetType?) -> Result<Unit> =
|
||||||
|
{ _, _, _, _, _ -> Result.success(Unit) }
|
||||||
|
|
||||||
|
override suspend fun sendLocation(body: String, geoUri: String, description: String?, zoomLevel: Int?, assetType: AssetType?): Result<Unit> = sendLocationLambda(
|
||||||
|
body,
|
||||||
|
geoUri,
|
||||||
|
description,
|
||||||
|
zoomLevel,
|
||||||
|
assetType
|
||||||
|
)
|
||||||
|
|
||||||
|
var createPollLambda: (question: String, answers: List<String>, maxSelections: Int, pollKind: PollKind) -> Result<Unit> =
|
||||||
|
{ _, _, _, _ -> Result.success(Unit) }
|
||||||
|
|
||||||
|
override suspend fun createPoll(question: String, answers: List<String>, maxSelections: Int, pollKind: PollKind): Result<Unit> = createPollLambda(
|
||||||
|
question,
|
||||||
|
answers,
|
||||||
|
maxSelections,
|
||||||
|
pollKind
|
||||||
|
)
|
||||||
|
|
||||||
|
var editPollLambda: (pollStartId: EventId, question: String, answers: List<String>, maxSelections: Int, pollKind: PollKind) -> Result<Unit> =
|
||||||
|
{ _, _, _, _, _ -> Result.success(Unit) }
|
||||||
|
|
||||||
|
override suspend fun editPoll(pollStartId: EventId, question: String, answers: List<String>, maxSelections: Int, pollKind: PollKind): Result<Unit> = editPollLambda(
|
||||||
|
pollStartId,
|
||||||
|
question,
|
||||||
|
answers,
|
||||||
|
maxSelections,
|
||||||
|
pollKind
|
||||||
|
)
|
||||||
|
|
||||||
|
var sendPollResponseLambda: (pollStartId: EventId, answers: List<String>) -> Result<Unit> = { _, _ -> Result.success(Unit) }
|
||||||
|
override suspend fun sendPollResponse(pollStartId: EventId, answers: List<String>): Result<Unit> = sendPollResponseLambda(pollStartId, answers)
|
||||||
|
|
||||||
|
var endPollLambda: (pollStartId: EventId, text: String) -> Result<Unit> = { _, _ -> Result.success(Unit) }
|
||||||
|
override suspend fun endPoll(pollStartId: EventId, text: String): Result<Unit> = endPollLambda(pollStartId, text)
|
||||||
|
|
||||||
|
var sendVoiceMessageLambda: (file: File, audioInfo: AudioInfo, waveform: List<Float>, progressCallback: ProgressCallback?) -> Result<MediaUploadHandler> =
|
||||||
|
{ _, _, _, _ -> Result.success(FakeMediaUploadHandler()) }
|
||||||
|
|
||||||
|
override suspend fun sendVoiceMessage(file: File, audioInfo: AudioInfo, waveform: List<Float>, progressCallback: ProgressCallback?): Result<MediaUploadHandler> = sendVoiceMessageLambda(
|
||||||
|
file,
|
||||||
|
audioInfo,
|
||||||
|
waveform,
|
||||||
|
progressCallback
|
||||||
|
)
|
||||||
|
|
||||||
|
var sendReadReceiptLambda: (eventId: EventId, receiptType: ReceiptType) -> Result<Unit> = { _, _ -> Result.success(Unit) }
|
||||||
|
override suspend fun sendReadReceipt(
|
||||||
|
eventId: EventId,
|
||||||
|
receiptType: ReceiptType,
|
||||||
|
): Result<Unit> = sendReadReceiptLambda(eventId, receiptType)
|
||||||
|
|
||||||
|
var paginateLambda: (direction: Timeline.PaginationDirection) -> Result<Boolean> = { Result.success(false) }
|
||||||
|
override suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> = paginateLambda(direction)
|
||||||
|
|
||||||
|
override fun paginationStatus(direction: Timeline.PaginationDirection): StateFlow<Timeline.PaginationStatus> {
|
||||||
|
return when (direction) {
|
||||||
|
Timeline.PaginationDirection.BACKWARDS -> backwardPaginationStatus
|
||||||
|
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() = Unit
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue