Timeline : start reworking timeline apis

This commit is contained in:
ganfra 2024-04-17 21:32:02 +02:00
parent 2ae6931486
commit 83e7c13618
33 changed files with 593 additions and 471 deletions

View file

@ -382,20 +382,19 @@ private fun MessagesViewContent(
},
content = { paddingValues ->
TimelineView(
modifier = Modifier.padding(paddingValues),
state = state.timelineState,
roomName = state.roomName.dataOrNull(),
typingNotificationState = state.typingNotificationState,
onMessageClicked = onMessageClicked,
onMessageLongClicked = onMessageLongClicked,
onUserDataClicked = onUserDataClicked,
onLinkClicked = onLinkClicked,
onMessageClicked = onMessageClicked,
onMessageLongClicked = onMessageLongClicked,
onTimestampClicked = onTimestampClicked,
onSwipeToReply = onSwipeToReply,
onReactionClicked = onReactionClicked,
onReactionLongClicked = onReactionLongClicked,
onMoreReactionsClicked = onMoreReactionsClicked,
onReadReceiptClick = onReadReceiptClick,
onSwipeToReply = onSwipeToReply,
modifier = Modifier.padding(paddingValues),
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
)
},

View file

@ -19,7 +19,6 @@ package io.element.android.features.messages.impl.timeline
import io.element.android.libraries.matrix.api.core.EventId
sealed interface TimelineEvents {
data object LoadMore : TimelineEvents
data class SetHighlightedEvent(val eventId: EventId?) : TimelineEvents
data class OnScrollFinished(val firstIndex: Int) : TimelineEvents
@ -28,6 +27,8 @@ sealed interface TimelineEvents {
*/
sealed interface EventFromTimelineItem : TimelineEvents
data class LoadMore(val backwards: Boolean) : EventFromTimelineItem
/**
* Events coming from a poll item.
*/

View file

@ -54,9 +54,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
private const val BACK_PAGINATION_EVENT_LIMIT = 20
private const val BACK_PAGINATION_PAGE_SIZE = 50
class TimelinePresenter @AssistedInject constructor(
private val timelineItemsFactory: TimelineItemsFactory,
private val room: MatrixRoom,
@ -73,7 +70,7 @@ class TimelinePresenter @AssistedInject constructor(
fun create(navigator: MessagesNavigator): TimelinePresenter
}
private val timeline = room.timeline
private val timeline = room.liveTimeline
@Composable
override fun present(): TimelineState {
@ -85,7 +82,7 @@ class TimelinePresenter @AssistedInject constructor(
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
val timelineItems by timelineItemsFactory.collectItemsAsState()
val paginationState by timeline.paginationState.collectAsState()
val paginationState by timeline.backPaginationStatus.collectAsState()
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
@ -99,7 +96,13 @@ class TimelinePresenter @AssistedInject constructor(
fun handleEvents(event: TimelineEvents) {
when (event) {
TimelineEvents.LoadMore -> localScope.paginateBackwards()
is TimelineEvents.LoadMore -> {
if(event.backwards) {
localScope.paginateBackwards()
}else{
//TODO implement pagination forward
}
}
is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId
is TimelineEvents.OnScrollFinished -> {
if (event.firstIndex == 0) {
@ -152,6 +155,7 @@ class TimelinePresenter @AssistedInject constructor(
val timelineRoomInfo by remember {
derivedStateOf {
TimelineRoomInfo(
name = room.displayName,
isDm = room.isDm,
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
@ -161,7 +165,7 @@ class TimelinePresenter @AssistedInject constructor(
return TimelineState(
timelineRoomInfo = timelineRoomInfo,
highlightedEventId = highlightedEventId.value,
paginationState = paginationState,
backPaginationStatus = paginationState,
timelineItems = timelineItems,
renderReadReceipts = renderReadReceipts,
newEventState = newItemState.value,
@ -233,6 +237,6 @@ class TimelinePresenter @AssistedInject constructor(
}
private fun CoroutineScope.paginateBackwards() = launch {
timeline.paginateBackwards(BACK_PAGINATION_EVENT_LIMIT, BACK_PAGINATION_PAGE_SIZE)
timeline.paginateBackwards()
}
}

View file

@ -20,7 +20,7 @@ import androidx.compose.runtime.Immutable
import io.element.android.features.messages.impl.timeline.model.NewEventState
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.Timeline
import kotlinx.collections.immutable.ImmutableList
@Immutable
@ -29,7 +29,7 @@ data class TimelineState(
val timelineRoomInfo: TimelineRoomInfo,
val renderReadReceipts: Boolean,
val highlightedEventId: EventId?,
val paginationState: MatrixTimeline.PaginationState,
val backPaginationStatus: Timeline.PaginationStatus,
val newEventState: NewEventState,
val eventSink: (TimelineEvents) -> Unit
)
@ -37,6 +37,7 @@ data class TimelineState(
@Immutable
data class TimelineRoomInfo(
val isDm: Boolean,
val name: String?,
val userHasPermissionToSendMessage: Boolean,
val userHasPermissionToSendReaction: Boolean,
)

View file

@ -34,7 +34,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
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.core.UserId
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
import kotlinx.collections.immutable.ImmutableList
@ -46,32 +46,31 @@ import kotlin.random.Random
fun aTimelineState(
timelineItems: ImmutableList<TimelineItem> = persistentListOf(),
paginationState: MatrixTimeline.PaginationState = aPaginationState(),
paginationState: Timeline.PaginationStatus = aPaginationStatus(),
renderReadReceipts: Boolean = false,
timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(),
eventSink: (TimelineEvents) -> Unit = {},
) = TimelineState(
timelineItems = timelineItems,
timelineRoomInfo = timelineRoomInfo,
paginationState = paginationState,
backPaginationStatus = paginationState,
renderReadReceipts = renderReadReceipts,
highlightedEventId = null,
newEventState = NewEventState.None,
eventSink = eventSink,
)
fun aPaginationState(
isBackPaginating: Boolean = false,
hasMoreToLoadBackwards: Boolean = true,
beginningOfRoomReached: Boolean = false,
): MatrixTimeline.PaginationState {
return MatrixTimeline.PaginationState(
isBackPaginating = isBackPaginating,
hasMoreToLoadBackwards = hasMoreToLoadBackwards,
beginningOfRoomReached = beginningOfRoomReached,
fun aPaginationStatus(
isPaginating: Boolean = false,
hasMoreToLoad: Boolean = true,
): Timeline.PaginationStatus {
return Timeline.PaginationStatus(
isPaginating = isPaginating,
hasMoreToLoad = hasMoreToLoad,
)
}
internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList<TimelineItem> {
return persistentListOf(
// 3 items (First Middle Last) with isMine = false
@ -230,10 +229,12 @@ internal fun aGroupedEvents(
}
internal fun aTimelineRoomInfo(
name: String = "Room name",
isDm: Boolean = false,
userHasPermissionToSendMessage: Boolean = true,
) = TimelineRoomInfo(
isDm = isDm,
name = name,
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToSendReaction = true,
)

View file

@ -55,7 +55,6 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.messages.impl.timeline.components.TimelineItemRow
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemRoomBeginningView
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories
@ -79,7 +78,6 @@ import kotlinx.coroutines.launch
fun TimelineView(
state: TimelineState,
typingNotificationState: TypingNotificationState,
roomName: String?,
onUserDataClicked: (UserId) -> Unit,
onLinkClicked: (String) -> Unit,
onMessageClicked: (TimelineItem.Event) -> Unit,
@ -93,9 +91,6 @@ fun TimelineView(
modifier: Modifier = Modifier,
forceJumpToBottomVisibility: Boolean = false
) {
fun onReachedLoadMore() {
state.eventSink(TimelineEvents.LoadMore)
}
fun onScrollFinishedAt(firstVisibleIndex: Int) {
state.eventSink(TimelineEvents.OnScrollFinished(firstVisibleIndex))
@ -152,20 +147,6 @@ fun TimelineView(
onSwipeToReply = onSwipeToReply,
)
}
if (state.paginationState.hasMoreToLoadBackwards) {
// Do not use key parameter to avoid wrong positioning
item(contentType = "TimelineLoadingMoreIndicator") {
TimelineLoadingMoreIndicator()
LaunchedEffect(Unit) {
onReachedLoadMore()
}
}
}
if (state.paginationState.beginningOfRoomReached && !state.timelineRoomInfo.isDm) {
item(contentType = "BeginningOfRoomReached") {
TimelineItemRoomBeginningView(roomName = roomName)
}
}
}
TimelineScrollHelper(
@ -272,17 +253,16 @@ internal fun TimelineViewPreview(
) {
TimelineView(
state = aTimelineState(timelineItems),
roomName = null,
typingNotificationState = aTypingNotificationState(),
onMessageClicked = {},
onTimestampClicked = {},
onUserDataClicked = {},
onLinkClicked = {},
onMessageClicked = {},
onMessageLongClicked = {},
onTimestampClicked = {},
onSwipeToReply = {},
onReactionClicked = { _, _ -> },
onReactionLongClicked = { _, _ -> },
onMoreReactionsClicked = {},
onSwipeToReply = {},
onReadReceiptClick = {},
forceJumpToBottomVisibility = true,
)

View file

@ -51,6 +51,8 @@ internal fun TimelineItemRow(
is TimelineItem.Virtual -> {
TimelineItemVirtualRow(
virtual = timelineItem,
timelineRoomInfo = timelineRoomInfo,
eventSink = eventSink,
modifier = modifier,
)
}

View file

@ -17,23 +17,39 @@
package io.element.android.features.messages.impl.timeline.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import io.element.android.features.messages.impl.timeline.TimelineEvents
import io.element.android.features.messages.impl.timeline.TimelineRoomInfo
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineEncryptedHistoryBannerView
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemReadMarkerView
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemRoomBeginningView
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemRoomBeginningModel
@Composable
fun TimelineItemVirtualRow(
virtual: TimelineItem.Virtual,
timelineRoomInfo: TimelineRoomInfo,
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
modifier: Modifier = Modifier
) {
when (virtual.model) {
is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier)
TimelineItemReadMarkerModel -> TimelineItemReadMarkerView()
is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(modifier)
TimelineItemRoomBeginningModel -> TimelineItemRoomBeginningView(roomName = timelineRoomInfo.name, modifier = modifier)
is TimelineItemLoadingIndicatorModel -> {
TimelineLoadingMoreIndicator()
LaunchedEffect(key1 = virtual.model.timestamp) {
eventSink(TimelineEvents.LoadMore(virtual.model.backwards))
}
}
}
}

View file

@ -18,7 +18,9 @@ package io.element.android.features.messages.impl.timeline.factories.virtual
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemRoomBeginningModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
@ -41,6 +43,11 @@ class TimelineItemVirtualFactory @Inject constructor(
is VirtualTimelineItem.DayDivider -> daySeparatorFactory.create(inner)
is VirtualTimelineItem.ReadMarker -> TimelineItemReadMarkerModel
is VirtualTimelineItem.EncryptedHistoryBanner -> TimelineItemEncryptedHistoryBannerVirtualModel
is VirtualTimelineItem.RoomBeginning -> TimelineItemRoomBeginningModel
is VirtualTimelineItem.LoadingIndicator -> TimelineItemLoadingIndicatorModel(
backwards = inner.backwards,
timestamp = inner.timestamp
)
}
}
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.model.virtual
data class TimelineItemLoadingIndicatorModel(
val backwards: Boolean,
val timestamp: Long,
) : TimelineItemVirtualModel {
override val type: String = "TimelineItemLoadingIndicatorModel"
}

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.model.virtual
data object TimelineItemRoomBeginningModel : TimelineItemVirtualModel {
override val type: String = "TimelineItemRoomBeginningModel"
}

View file

@ -36,7 +36,6 @@ import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
@ -96,15 +95,15 @@ class TimelinePresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.paginationState.hasMoreToLoadBackwards).isTrue()
assertThat(initialState.paginationState.isBackPaginating).isFalse()
assertThat(initialState.backPaginationStatus.hasMoreToLoadBackwards).isTrue()
assertThat(initialState.backPaginationStatus.isBackPaginating).isFalse()
initialState.eventSink.invoke(TimelineEvents.LoadMore)
val inPaginationState = awaitItem()
assertThat(inPaginationState.paginationState.isBackPaginating).isTrue()
assertThat(inPaginationState.paginationState.hasMoreToLoadBackwards).isTrue()
assertThat(inPaginationState.backPaginationStatus.isBackPaginating).isTrue()
assertThat(inPaginationState.backPaginationStatus.hasMoreToLoadBackwards).isTrue()
val postPaginationState = awaitItem()
assertThat(postPaginationState.paginationState.hasMoreToLoadBackwards).isTrue()
assertThat(postPaginationState.paginationState.isBackPaginating).isFalse()
assertThat(postPaginationState.backPaginationStatus.hasMoreToLoadBackwards).isTrue()
assertThat(postPaginationState.backPaginationStatus.isBackPaginating).isFalse()
}
}

View file

@ -43,7 +43,6 @@ class TimelineViewTest {
)
),
typingNotificationState = aTypingNotificationState(),
roomName = null,
onUserDataClicked = EnsureNeverCalledWithParam(),
onLinkClicked = EnsureNeverCalledWithParam(),
onMessageClicked = EnsureNeverCalledWithParam(),
@ -71,7 +70,6 @@ class TimelineViewTest {
)
),
typingNotificationState = aTypingNotificationState(),
roomName = null,
onUserDataClicked = EnsureNeverCalledWithParam(),
onLinkClicked = EnsureNeverCalledWithParam(),
onMessageClicked = EnsureNeverCalledWithParam(),