Threads - first iteration (#5165)

* Initial threads support: parse `ThreadSummary`.

Replace several `isThreaded` values with `EventThreadInfo`, which contains the info about the event either being the root of a thread or part of it.

* Add `Threaded` timeline mode

* Add a `liveTimeline` parameter to `TimelineController`'s  constructor. This way we can customise which timeline will be used as the 'live' one. Also add `@LiveTimeline` DI qualifier for the actual live timeline of the room.

* Create `ThreadedMessagesNode`. Allow opening a thread in a separate screen.

* Add the callbacks for the list menu actions - even if they're the wrong ones and will send the data to the room instead

* Send attachments and location in threads

* Fix polls in threads, add support for sending voice messages in threads

* Display thread summaries only when the feature flag is enabled

* Use 'Reply' instead of 'Reply in thread' when in threaded timeline mode

* Remove incorrect usage of `Timeline` in `MessageComposerPresenter`. This led to replies to threaded events not appearing as actual replies.

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa 2025-08-19 15:35:48 +02:00 committed by GitHub
parent cc10ba41fd
commit 35928e3630
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
119 changed files with 1520 additions and 339 deletions

View file

@ -14,6 +14,7 @@ import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.RoomAlias
@ -133,6 +134,7 @@ class RustMatrixClient(
baseCacheDirectory: File,
clock: SystemClock,
timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
private val featureFlagService: FeatureFlagService,
) : MatrixClient {
override val sessionId: UserId = UserId(innerClient.userId())
override val deviceId: DeviceId = DeviceId(innerClient.deviceId())
@ -203,6 +205,7 @@ class RustMatrixClient(
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
roomMembershipObserver = roomMembershipObserver,
roomInfoMapper = roomInfoMapper,
featureFlagService = featureFlagService,
)
override val mediaLoader: MatrixMediaLoader = RustMediaLoader(

View file

@ -93,6 +93,7 @@ class RustMatrixClientFactory @Inject constructor(
baseCacheDirectory = cacheDirectory,
clock = clock,
timelineEventTypeFilterFactory = timelineEventTypeFilterFactory,
featureFlagService = featureFlagService,
).also {
Timber.tag(it.toString()).d("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'")
}
@ -131,6 +132,7 @@ class RustMatrixClientFactory @Inject constructor(
)
)
.enableShareHistoryOnInvite(featureFlagService.isFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite))
.threadsEnabled(featureFlagService.isFeatureEnabled(FeatureFlags.HideThreadedEvents), threadSubscriptions = false)
.run {
// Apply sliding sync version settings
when (slidingSyncType) {

View file

@ -11,6 +11,8 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomAlias
@ -83,6 +85,7 @@ class JoinedRustRoom(
private val coroutineDispatchers: CoroutineDispatchers,
private val systemClock: SystemClock,
private val roomContentForwarder: RoomContentForwarder,
private val featureFlagService: FeatureFlagService,
) : JoinedRoom, BaseRoom by baseRoom {
// Create a dispatcher for all room methods...
private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32)
@ -132,7 +135,7 @@ class JoinedRustRoom(
override val roomNotificationSettingsStateFlow = MutableStateFlow<RoomNotificationSettingsState>(RoomNotificationSettingsState.Unknown)
override val liveTimeline = liveInnerTimeline.map(mode = Timeline.Mode.LIVE) {
override val liveTimeline = liveInnerTimeline.map(mode = Timeline.Mode.Live) {
syncUpdateFlow.value = systemClock.epochMillis()
}
@ -153,22 +156,27 @@ class JoinedRustRoom(
override suspend fun createTimeline(
createTimelineParams: CreateTimelineParams,
): Result<Timeline> = withContext(roomDispatcher) {
val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.HideThreadedEvents)
val focus = when (createTimelineParams) {
is CreateTimelineParams.PinnedOnly -> TimelineFocus.PinnedEvents(
maxEventsToLoad = 100u,
maxConcurrentRequests = 10u,
)
is CreateTimelineParams.MediaOnly -> TimelineFocus.Live(hideThreadedEvents = false)
is CreateTimelineParams.MediaOnly -> TimelineFocus.Live(hideThreadedEvents = hideThreadedEvents)
is CreateTimelineParams.Focused -> TimelineFocus.Event(
eventId = createTimelineParams.focusedEventId.value,
numContextEvents = 50u,
hideThreadedEvents = false,
hideThreadedEvents = hideThreadedEvents,
)
is CreateTimelineParams.MediaOnlyFocused -> TimelineFocus.Event(
eventId = createTimelineParams.focusedEventId.value,
numContextEvents = 50u,
// Never hide threaded events in media focused timeline
hideThreadedEvents = false,
)
is CreateTimelineParams.Threaded -> TimelineFocus.Thread(
rootEventId = createTimelineParams.threadRootEventId.value,
)
}
val filter = when (createTimelineParams) {
@ -182,7 +190,8 @@ class JoinedRustRoom(
)
)
is CreateTimelineParams.Focused,
CreateTimelineParams.PinnedOnly -> TimelineFilter.All
CreateTimelineParams.PinnedOnly,
is CreateTimelineParams.Threaded -> TimelineFilter.All
}
val internalIdPrefix = when (createTimelineParams) {
@ -190,6 +199,7 @@ class JoinedRustRoom(
is CreateTimelineParams.Focused -> "focus_${createTimelineParams.focusedEventId}"
is CreateTimelineParams.MediaOnly -> "MediaGallery_"
is CreateTimelineParams.MediaOnlyFocused -> "MediaGallery_${createTimelineParams.focusedEventId}"
is CreateTimelineParams.Threaded -> "Thread_${createTimelineParams.threadRootEventId}"
}
// Note that for TimelineFilter.MediaOnlyFocused, the date separator will be filtered out,
@ -198,7 +208,8 @@ class JoinedRustRoom(
is CreateTimelineParams.MediaOnly,
is CreateTimelineParams.MediaOnlyFocused -> DateDividerMode.MONTHLY
is CreateTimelineParams.Focused,
CreateTimelineParams.PinnedOnly -> DateDividerMode.DAILY
CreateTimelineParams.PinnedOnly,
is CreateTimelineParams.Threaded -> DateDividerMode.DAILY
}
// Track read receipts only for focused timeline for performance optimization
@ -216,17 +227,19 @@ class JoinedRustRoom(
)
).let { innerTimeline ->
val mode = when (createTimelineParams) {
is CreateTimelineParams.Focused -> Timeline.Mode.FOCUSED_ON_EVENT
is CreateTimelineParams.MediaOnly -> Timeline.Mode.MEDIA
is CreateTimelineParams.MediaOnlyFocused -> Timeline.Mode.FOCUSED_ON_EVENT
CreateTimelineParams.PinnedOnly -> Timeline.Mode.PINNED_EVENTS
is CreateTimelineParams.Focused -> Timeline.Mode.FocusedOnEvent(createTimelineParams.focusedEventId)
is CreateTimelineParams.MediaOnly -> Timeline.Mode.Media
is CreateTimelineParams.MediaOnlyFocused -> Timeline.Mode.FocusedOnEvent(createTimelineParams.focusedEventId)
CreateTimelineParams.PinnedOnly -> Timeline.Mode.PinnedEvents
is CreateTimelineParams.Threaded -> Timeline.Mode.Thread(createTimelineParams.threadRootEventId)
}
innerTimeline.map(mode = mode)
}
}.mapFailure {
when (createTimelineParams) {
is CreateTimelineParams.Focused,
is CreateTimelineParams.MediaOnlyFocused -> it.toFocusEventException()
is CreateTimelineParams.MediaOnlyFocused,
is CreateTimelineParams.Threaded -> it.toFocusEventException()
CreateTimelineParams.MediaOnly,
CreateTimelineParams.PinnedOnly -> it
}

View file

@ -9,6 +9,8 @@ package io.element.android.libraries.matrix.impl.room
import io.element.android.appconfig.TimelineConfig
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
@ -48,6 +50,7 @@ class RustRoomFactory(
private val innerRoomListService: InnerRoomListService,
private val roomSyncSubscriber: RoomSyncSubscriber,
private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
private val featureFlagService: FeatureFlagService,
private val roomMembershipObserver: RoomMembershipObserver,
private val roomInfoMapper: RoomInfoMapper,
) {
@ -105,10 +108,11 @@ class RustRoomFactory(
val sdkRoom = awaitRoomInRoomList(roomId) ?: return@withContext null
if (sdkRoom.membership() == Membership.JOINED) {
val hideThreadedEvents = featureFlagService.isFeatureEnabled(FeatureFlags.HideThreadedEvents)
// Init the live timeline in the SDK from the Room
val timeline = sdkRoom.timelineWithConfiguration(
TimelineConfiguration(
focus = TimelineFocus.Live(hideThreadedEvents = false),
focus = TimelineFocus.Live(hideThreadedEvents = hideThreadedEvents),
filter = eventFilters?.let(TimelineFilter::EventTypeFilter) ?: TimelineFilter.All,
internalIdPrefix = "live",
dateDividerMode = DateDividerMode.DAILY,
@ -125,6 +129,7 @@ class RustRoomFactory(
liveInnerTimeline = timeline,
coroutineDispatchers = dispatchers,
systemClock = systemClock,
featureFlagService = featureFlagService,
)
)
} else {

View file

@ -79,7 +79,7 @@ private const val PAGINATION_SIZE = 50
class RustTimeline(
private val inner: InnerTimeline,
mode: Timeline.Mode,
override val mode: Timeline.Mode,
systemClock: SystemClock,
private val joinedRoom: JoinedRoom,
private val coroutineScope: CoroutineScope,
@ -118,19 +118,20 @@ class RustTimeline(
private val typingNotificationPostProcessor = TypingNotificationPostProcessor(mode)
override val backwardPaginationStatus = MutableStateFlow(
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode != Timeline.Mode.PINNED_EVENTS)
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode != Timeline.Mode.PinnedEvents)
)
override val forwardPaginationStatus = MutableStateFlow(
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode == Timeline.Mode.FOCUSED_ON_EVENT)
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode !is Timeline.Mode.FocusedOnEvent)
)
init {
if (mode != Timeline.Mode.PINNED_EVENTS) {
coroutineScope.fetchMembers()
when (mode) {
is Timeline.Mode.Live, is Timeline.Mode.FocusedOnEvent -> coroutineScope.fetchMembers()
else -> Unit
}
if (mode == Timeline.Mode.LIVE) {
if (mode == Timeline.Mode.Live) {
// When timeline is live, we need to listen to the back pagination status as
// sdk can automatically paginate backwards.
coroutineScope.registerBackPaginationStatusListener()
@ -219,6 +220,7 @@ class RustTimeline(
items = items,
hasMoreToLoadBackward = backwardPaginationStatus.hasMoreToLoad,
hasMoreToLoadForward = forwardPaginationStatus.hasMoreToLoad,
timelineMode = mode,
)
}
.let { items ->

View file

@ -7,6 +7,7 @@
package io.element.android.libraries.matrix.impl.timeline.item.event
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType
@ -37,14 +38,14 @@ private const val MSG_TYPE_GALLERY_UNSTABLE = "dm.filament.gallery"
class EventMessageMapper {
private val inReplyToMapper by lazy { InReplyToMapper(TimelineEventContentMapper()) }
fun map(message: MsgLikeKind.Message, inReplyTo: InReplyToDetails?, isThreaded: Boolean): MessageContent = message.use {
fun map(message: MsgLikeKind.Message, inReplyTo: InReplyToDetails?, threadInfo: EventThreadInfo): MessageContent = message.use {
val type = it.content.msgType.use(this::mapMessageType)
val inReplyToEvent: InReplyTo? = inReplyTo?.use(inReplyToMapper::map)
MessageContent(
body = it.content.body,
inReplyTo = inReplyToEvent,
isEdited = it.content.isEdited,
isThreaded = isThreaded,
threadInfo = threadInfo,
type = type
)
}

View file

@ -0,0 +1,18 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.timeline.item.event
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import org.matrix.rustcomponents.sdk.EventOrTransactionId as RustEventOrTransactionId
fun RustEventOrTransactionId.map(): EventOrTransactionId = when (this) {
is RustEventOrTransactionId.EventId -> EventOrTransactionId.Event(EventId(eventId))
is RustEventOrTransactionId.TransactionId -> EventOrTransactionId.Transaction(TransactionId(transactionId))
}

View file

@ -7,7 +7,12 @@
package io.element.android.libraries.matrix.impl.timeline.item.event
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.EmbeddedEventInfo
import io.element.android.libraries.matrix.api.timeline.item.EventThreadInfo
import io.element.android.libraries.matrix.api.timeline.item.ThreadSummary
import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
@ -27,6 +32,7 @@ import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.poll.map
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import org.matrix.rustcomponents.sdk.EmbeddedEventDetails
import org.matrix.rustcomponents.sdk.MsgLikeKind
import org.matrix.rustcomponents.sdk.TimelineItemContent
import org.matrix.rustcomponents.sdk.use
@ -59,8 +65,35 @@ class TimelineEventContentMapper(
when (val kind = it.content.kind) {
is MsgLikeKind.Message -> {
val inReplyTo = it.content.inReplyTo
val isThreaded = it.content.threadRoot != null
eventMessageMapper.map(kind, inReplyTo, isThreaded)
val threadSummary = it.content.threadSummary?.use { summary ->
val numberOfReplies = summary.numReplies().toLong()
val latestEvent = summary.latestEvent()
val details = when (latestEvent) {
is EmbeddedEventDetails.Unavailable -> AsyncData.Uninitialized
is EmbeddedEventDetails.Pending -> AsyncData.Loading()
is EmbeddedEventDetails.Error -> AsyncData.Failure(IllegalStateException(latestEvent.message))
is EmbeddedEventDetails.Ready -> {
AsyncData.Success(
EmbeddedEventInfo(
eventOrTransactionId = latestEvent.eventOrTransactionId.map(),
content = map(latestEvent.content),
senderId = UserId(latestEvent.sender),
senderProfile = latestEvent.senderProfile.map(),
timestamp = latestEvent.timestamp.toLong()
)
)
}
}
ThreadSummary(
latestEvent = details,
numberOfReplies = numberOfReplies,
)
}
val threadInfo = EventThreadInfo(
threadRootId = it.content.threadRoot?.let(::ThreadId),
threadSummary = threadSummary,
)
eventMessageMapper.map(kind, inReplyTo, threadInfo)
}
is MsgLikeKind.Redacted -> {
RedactedContent

View file

@ -24,7 +24,7 @@ class LastForwardIndicatorsPostProcessor(
items: List<MatrixTimelineItem>,
): List<MatrixTimelineItem> {
// We don't need to add the last forward indicator if we are not in the FOCUSED_ON_EVENT mode
if (mode != Timeline.Mode.FOCUSED_ON_EVENT) {
if (mode !is Timeline.Mode.FocusedOnEvent) {
return items
} else {
return buildList {

View file

@ -18,8 +18,9 @@ class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) {
items: List<MatrixTimelineItem>,
hasMoreToLoadBackward: Boolean,
hasMoreToLoadForward: Boolean,
timelineMode: Timeline.Mode,
): List<MatrixTimelineItem> {
val shouldAddForwardLoadingIndicator = hasMoreToLoadForward && items.isNotEmpty()
val shouldAddForwardLoadingIndicator = timelineMode is Timeline.Mode.Live && hasMoreToLoadForward && items.isNotEmpty()
val currentTimestamp = systemClock.epochMillis()
return buildList {
if (hasMoreToLoadBackward) {

View file

@ -28,7 +28,7 @@ class RoomBeginningPostProcessor(private val mode: Timeline.Mode) {
): List<MatrixTimelineItem> {
return when {
items.isEmpty() -> items
mode == Timeline.Mode.PINNED_EVENTS -> items
mode == Timeline.Mode.PinnedEvents -> items
isDm -> processForDM(items, roomCreator)
hasMoreToLoadBackwards -> items
else -> processForRoom(items)

View file

@ -17,7 +17,7 @@ import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTime
*/
class TypingNotificationPostProcessor(private val mode: Timeline.Mode) {
fun process(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> {
return if (mode == Timeline.Mode.LIVE) {
return if (mode is Timeline.Mode.Live) {
buildList {
addAll(items)
add(

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSyncService
import io.element.android.libraries.matrix.impl.room.FakeTimelineEventTypeFilterFactory
@ -66,5 +67,6 @@ class RustMatrixClientTest {
baseCacheDirectory = File(""),
clock = FakeSystemClock(),
timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(),
featureFlagService = FakeFeatureFlagService(),
)
}

View file

@ -39,6 +39,7 @@ class FakeFfiClientBuilder : ClientBuilder(NoPointer) {
override fun userAgent(userAgent: String) = this
override fun username(username: String) = this
override fun enableShareHistoryOnInvite(enableShareHistoryOnInvite: Boolean): ClientBuilder = this
override fun threadsEnabled(enabled: Boolean, threadSubscriptions: Boolean): ClientBuilder = this
override suspend fun build(): Client {
return FakeFfiClient(withUtdHook = {})

View file

@ -90,7 +90,7 @@ class RustTimelineTest {
private fun TestScope.createRustTimeline(
inner: InnerTimeline,
mode: Timeline.Mode = Timeline.Mode.LIVE,
mode: Timeline.Mode = Timeline.Mode.Live,
systemClock: SystemClock = FakeSystemClock(),
joinedRoom: JoinedRoom = FakeJoinedRoom().apply { givenRoomInfo(aRoomInfo()) },
coroutineScope: CoroutineScope = backgroundScope,

View file

@ -12,19 +12,20 @@ import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import org.junit.Test
class LastForwardIndicatorsPostProcessorTest {
@Test
fun `LastForwardIndicatorsPostProcessor does not alter the items with mode not FOCUSED_ON_EVENT`() {
val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.LIVE)
val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.Live)
val result = sut.process(listOf(messageEvent))
assertThat(result).containsExactly(messageEvent)
}
@Test
fun `LastForwardIndicatorsPostProcessor add virtual items`() {
val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT)
val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FocusedOnEvent(AN_EVENT_ID))
val result = sut.process(listOf(messageEvent))
assertThat(result).containsExactly(
messageEvent,
@ -37,7 +38,7 @@ class LastForwardIndicatorsPostProcessorTest {
@Test
fun `LastForwardIndicatorsPostProcessor add virtual items on empty list`() {
val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT)
val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FocusedOnEvent(AN_EVENT_ID))
val result = sut.process(listOf())
assertThat(result).containsExactly(
MatrixTimelineItem.Virtual(
@ -49,7 +50,7 @@ class LastForwardIndicatorsPostProcessorTest {
@Test
fun `LastForwardIndicatorsPostProcessor add virtual items but does not alter the list if called a second time`() {
val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT)
val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FocusedOnEvent(AN_EVENT_ID))
// Process a first time
sut.process(listOf(messageEvent))
// Process a second time with the same Event
@ -65,7 +66,7 @@ class LastForwardIndicatorsPostProcessorTest {
@Test
fun `LastForwardIndicatorsPostProcessor add virtual items each time it is called with new Events`() {
val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FOCUSED_ON_EVENT)
val sut = LastForwardIndicatorsPostProcessor(Timeline.Mode.FocusedOnEvent(AN_EVENT_ID))
// Process a first time
sut.process(listOf(dayEvent, messageEvent))
// Process a second time with the same Event

View file

@ -24,6 +24,7 @@ class LoadingIndicatorsPostProcessorTest {
items = listOf(messageEvent, messageEvent2),
hasMoreToLoadBackward = true,
hasMoreToLoadForward = false,
timelineMode = Timeline.Mode.Live,
)
assertThat(result).containsExactly(
MatrixTimelineItem.Virtual(
@ -46,6 +47,7 @@ class LoadingIndicatorsPostProcessorTest {
items = listOf(messageEvent, messageEvent2),
hasMoreToLoadBackward = false,
hasMoreToLoadForward = true,
timelineMode = Timeline.Mode.Live,
)
assertThat(result).containsExactly(
messageEvent,
@ -68,6 +70,7 @@ class LoadingIndicatorsPostProcessorTest {
items = listOf(messageEvent, messageEvent2),
hasMoreToLoadBackward = true,
hasMoreToLoadForward = true,
timelineMode = Timeline.Mode.Live,
)
assertThat(result).containsExactly(
MatrixTimelineItem.Virtual(
@ -97,6 +100,7 @@ class LoadingIndicatorsPostProcessorTest {
items = listOf(),
hasMoreToLoadBackward = true,
hasMoreToLoadForward = true,
timelineMode = Timeline.Mode.Live,
)
assertThat(result).containsExactly(
MatrixTimelineItem.Virtual(

View file

@ -15,7 +15,7 @@ import org.junit.Test
class RoomBeginningPostProcessorTest {
@Test
fun `processor returns empty list when empty list is provided`() {
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
val processor = RoomBeginningPostProcessor(Timeline.Mode.Live)
val processedItems = processor.process(
items = emptyList(),
isDm = true,
@ -27,7 +27,7 @@ class RoomBeginningPostProcessorTest {
@Test
fun `processor returns the provided list when it only contains a message`() {
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
val processor = RoomBeginningPostProcessor(Timeline.Mode.Live)
val processedItems = processor.process(
items = listOf(messageEvent),
isDm = true,
@ -39,7 +39,7 @@ class RoomBeginningPostProcessorTest {
@Test
fun `processor returns the provided list when it only contains a message and the roomCreator is not provided`() {
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
val processor = RoomBeginningPostProcessor(Timeline.Mode.Live)
val processedItems = processor.process(
items = listOf(messageEvent),
isDm = true,
@ -56,7 +56,7 @@ class RoomBeginningPostProcessorTest {
roomCreateEvent,
roomCreatorJoinEvent,
)
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
val processor = RoomBeginningPostProcessor(Timeline.Mode.Live)
val processedItems = processor.process(
items = timelineItems,
isDm = true,
@ -72,7 +72,7 @@ class RoomBeginningPostProcessorTest {
roomCreateEvent,
roomCreatorJoinEvent,
)
val processor = RoomBeginningPostProcessor(Timeline.Mode.PINNED_EVENTS)
val processor = RoomBeginningPostProcessor(Timeline.Mode.PinnedEvents)
val processedItems = processor.process(
items = timelineItems,
isDm = true,
@ -94,7 +94,7 @@ class RoomBeginningPostProcessorTest {
otherMemberJoinEvent,
messageEvent,
)
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
val processor = RoomBeginningPostProcessor(Timeline.Mode.Live)
val processedItems = processor.process(timelineItems, isDm = true, roomCreator = A_USER_ID, hasMoreToLoadBackwards = false)
assertThat(processedItems).isEqualTo(expected)
}
@ -105,7 +105,7 @@ class RoomBeginningPostProcessorTest {
roomCreateEvent,
roomCreatorJoinEvent,
)
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
val processor = RoomBeginningPostProcessor(Timeline.Mode.Live)
val processedItems = processor.process(timelineItems, isDm = true, roomCreator = A_USER_ID, hasMoreToLoadBackwards = true)
assertThat(processedItems).isEmpty()
}
@ -115,7 +115,7 @@ class RoomBeginningPostProcessorTest {
val timelineItems = listOf(
roomCreatorJoinEvent,
)
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
val processor = RoomBeginningPostProcessor(Timeline.Mode.Live)
val processedItems = processor.process(timelineItems, isDm = true, roomCreator = A_USER_ID, hasMoreToLoadBackwards = true)
assertThat(processedItems).isEmpty()
}
@ -126,7 +126,7 @@ class RoomBeginningPostProcessorTest {
roomCreateEvent,
otherMemberJoinEvent,
)
val processor = RoomBeginningPostProcessor(Timeline.Mode.LIVE)
val processor = RoomBeginningPostProcessor(Timeline.Mode.Live)
val processedItems = processor.process(timelineItems, isDm = true, roomCreator = A_USER_ID, hasMoreToLoadBackwards = true)
assertThat(processedItems).isEqualTo(listOf(otherMemberJoinEvent))
}