Timeline : start reworking timeline apis
This commit is contained in:
parent
66e2515879
commit
e4174f2792
33 changed files with 593 additions and 471 deletions
|
|
@ -382,20 +382,19 @@ private fun MessagesViewContent(
|
||||||
},
|
},
|
||||||
content = { paddingValues ->
|
content = { paddingValues ->
|
||||||
TimelineView(
|
TimelineView(
|
||||||
modifier = Modifier.padding(paddingValues),
|
|
||||||
state = state.timelineState,
|
state = state.timelineState,
|
||||||
roomName = state.roomName.dataOrNull(),
|
|
||||||
typingNotificationState = state.typingNotificationState,
|
typingNotificationState = state.typingNotificationState,
|
||||||
onMessageClicked = onMessageClicked,
|
|
||||||
onMessageLongClicked = onMessageLongClicked,
|
|
||||||
onUserDataClicked = onUserDataClicked,
|
onUserDataClicked = onUserDataClicked,
|
||||||
onLinkClicked = onLinkClicked,
|
onLinkClicked = onLinkClicked,
|
||||||
|
onMessageClicked = onMessageClicked,
|
||||||
|
onMessageLongClicked = onMessageLongClicked,
|
||||||
onTimestampClicked = onTimestampClicked,
|
onTimestampClicked = onTimestampClicked,
|
||||||
|
onSwipeToReply = onSwipeToReply,
|
||||||
onReactionClicked = onReactionClicked,
|
onReactionClicked = onReactionClicked,
|
||||||
onReactionLongClicked = onReactionLongClicked,
|
onReactionLongClicked = onReactionLongClicked,
|
||||||
onMoreReactionsClicked = onMoreReactionsClicked,
|
onMoreReactionsClicked = onMoreReactionsClicked,
|
||||||
onReadReceiptClick = onReadReceiptClick,
|
onReadReceiptClick = onReadReceiptClick,
|
||||||
onSwipeToReply = onSwipeToReply,
|
modifier = Modifier.padding(paddingValues),
|
||||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ package io.element.android.features.messages.impl.timeline
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
|
||||||
sealed interface TimelineEvents {
|
sealed interface TimelineEvents {
|
||||||
data object LoadMore : TimelineEvents
|
|
||||||
data class SetHighlightedEvent(val eventId: EventId?) : TimelineEvents
|
data class SetHighlightedEvent(val eventId: EventId?) : TimelineEvents
|
||||||
data class OnScrollFinished(val firstIndex: Int) : TimelineEvents
|
data class OnScrollFinished(val firstIndex: Int) : TimelineEvents
|
||||||
|
|
||||||
|
|
@ -28,6 +27,8 @@ sealed interface TimelineEvents {
|
||||||
*/
|
*/
|
||||||
sealed interface EventFromTimelineItem : TimelineEvents
|
sealed interface EventFromTimelineItem : TimelineEvents
|
||||||
|
|
||||||
|
data class LoadMore(val backwards: Boolean) : EventFromTimelineItem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events coming from a poll item.
|
* Events coming from a poll item.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,6 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
private const val BACK_PAGINATION_EVENT_LIMIT = 20
|
|
||||||
private const val BACK_PAGINATION_PAGE_SIZE = 50
|
|
||||||
|
|
||||||
class TimelinePresenter @AssistedInject constructor(
|
class TimelinePresenter @AssistedInject constructor(
|
||||||
private val timelineItemsFactory: TimelineItemsFactory,
|
private val timelineItemsFactory: TimelineItemsFactory,
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
|
|
@ -73,7 +70,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
fun create(navigator: MessagesNavigator): TimelinePresenter
|
fun create(navigator: MessagesNavigator): TimelinePresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
private val timeline = room.timeline
|
private val timeline = room.liveTimeline
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): TimelineState {
|
override fun present(): TimelineState {
|
||||||
|
|
@ -85,7 +82,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
|
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
|
||||||
|
|
||||||
val timelineItems by timelineItemsFactory.collectItemsAsState()
|
val timelineItems by timelineItemsFactory.collectItemsAsState()
|
||||||
val paginationState by timeline.paginationState.collectAsState()
|
val paginationState by timeline.backPaginationStatus.collectAsState()
|
||||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||||
|
|
||||||
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
|
val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value)
|
||||||
|
|
@ -99,7 +96,13 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
|
|
||||||
fun handleEvents(event: TimelineEvents) {
|
fun handleEvents(event: TimelineEvents) {
|
||||||
when (event) {
|
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.SetHighlightedEvent -> highlightedEventId.value = event.eventId
|
||||||
is TimelineEvents.OnScrollFinished -> {
|
is TimelineEvents.OnScrollFinished -> {
|
||||||
if (event.firstIndex == 0) {
|
if (event.firstIndex == 0) {
|
||||||
|
|
@ -152,6 +155,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
val timelineRoomInfo by remember {
|
val timelineRoomInfo by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
TimelineRoomInfo(
|
TimelineRoomInfo(
|
||||||
|
name = room.displayName,
|
||||||
isDm = room.isDm,
|
isDm = room.isDm,
|
||||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||||
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
|
||||||
|
|
@ -161,7 +165,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
return TimelineState(
|
return TimelineState(
|
||||||
timelineRoomInfo = timelineRoomInfo,
|
timelineRoomInfo = timelineRoomInfo,
|
||||||
highlightedEventId = highlightedEventId.value,
|
highlightedEventId = highlightedEventId.value,
|
||||||
paginationState = paginationState,
|
backPaginationStatus = paginationState,
|
||||||
timelineItems = timelineItems,
|
timelineItems = timelineItems,
|
||||||
renderReadReceipts = renderReadReceipts,
|
renderReadReceipts = renderReadReceipts,
|
||||||
newEventState = newItemState.value,
|
newEventState = newItemState.value,
|
||||||
|
|
@ -233,6 +237,6 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.paginateBackwards() = launch {
|
private fun CoroutineScope.paginateBackwards() = launch {
|
||||||
timeline.paginateBackwards(BACK_PAGINATION_EVENT_LIMIT, BACK_PAGINATION_PAGE_SIZE)
|
timeline.paginateBackwards()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.NewEventState
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
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.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
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
@ -29,7 +29,7 @@ data class TimelineState(
|
||||||
val timelineRoomInfo: TimelineRoomInfo,
|
val timelineRoomInfo: TimelineRoomInfo,
|
||||||
val renderReadReceipts: Boolean,
|
val renderReadReceipts: Boolean,
|
||||||
val highlightedEventId: EventId?,
|
val highlightedEventId: EventId?,
|
||||||
val paginationState: MatrixTimeline.PaginationState,
|
val backPaginationStatus: Timeline.PaginationStatus,
|
||||||
val newEventState: NewEventState,
|
val newEventState: NewEventState,
|
||||||
val eventSink: (TimelineEvents) -> Unit
|
val eventSink: (TimelineEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
@ -37,6 +37,7 @@ data class TimelineState(
|
||||||
@Immutable
|
@Immutable
|
||||||
data class TimelineRoomInfo(
|
data class TimelineRoomInfo(
|
||||||
val isDm: Boolean,
|
val isDm: Boolean,
|
||||||
|
val name: String?,
|
||||||
val userHasPermissionToSendMessage: Boolean,
|
val userHasPermissionToSendMessage: Boolean,
|
||||||
val userHasPermissionToSendReaction: Boolean,
|
val userHasPermissionToSendReaction: Boolean,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.TransactionId
|
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.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.TimelineItemDebugInfo
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -46,32 +46,31 @@ import kotlin.random.Random
|
||||||
|
|
||||||
fun aTimelineState(
|
fun aTimelineState(
|
||||||
timelineItems: ImmutableList<TimelineItem> = persistentListOf(),
|
timelineItems: ImmutableList<TimelineItem> = persistentListOf(),
|
||||||
paginationState: MatrixTimeline.PaginationState = aPaginationState(),
|
paginationState: Timeline.PaginationStatus = aPaginationStatus(),
|
||||||
renderReadReceipts: Boolean = false,
|
renderReadReceipts: Boolean = false,
|
||||||
timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(),
|
timelineRoomInfo: TimelineRoomInfo = aTimelineRoomInfo(),
|
||||||
eventSink: (TimelineEvents) -> Unit = {},
|
eventSink: (TimelineEvents) -> Unit = {},
|
||||||
) = TimelineState(
|
) = TimelineState(
|
||||||
timelineItems = timelineItems,
|
timelineItems = timelineItems,
|
||||||
timelineRoomInfo = timelineRoomInfo,
|
timelineRoomInfo = timelineRoomInfo,
|
||||||
paginationState = paginationState,
|
backPaginationStatus = paginationState,
|
||||||
renderReadReceipts = renderReadReceipts,
|
renderReadReceipts = renderReadReceipts,
|
||||||
highlightedEventId = null,
|
highlightedEventId = null,
|
||||||
newEventState = NewEventState.None,
|
newEventState = NewEventState.None,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aPaginationState(
|
fun aPaginationStatus(
|
||||||
isBackPaginating: Boolean = false,
|
isPaginating: Boolean = false,
|
||||||
hasMoreToLoadBackwards: Boolean = true,
|
hasMoreToLoad: Boolean = true,
|
||||||
beginningOfRoomReached: Boolean = false,
|
): Timeline.PaginationStatus {
|
||||||
): MatrixTimeline.PaginationState {
|
return Timeline.PaginationStatus(
|
||||||
return MatrixTimeline.PaginationState(
|
isPaginating = isPaginating,
|
||||||
isBackPaginating = isBackPaginating,
|
hasMoreToLoad = hasMoreToLoad,
|
||||||
hasMoreToLoadBackwards = hasMoreToLoadBackwards,
|
|
||||||
beginningOfRoomReached = beginningOfRoomReached,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList<TimelineItem> {
|
internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList<TimelineItem> {
|
||||||
return persistentListOf(
|
return persistentListOf(
|
||||||
// 3 items (First Middle Last) with isMine = false
|
// 3 items (First Middle Last) with isMine = false
|
||||||
|
|
@ -230,10 +229,12 @@ internal fun aGroupedEvents(
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun aTimelineRoomInfo(
|
internal fun aTimelineRoomInfo(
|
||||||
|
name: String = "Room name",
|
||||||
isDm: Boolean = false,
|
isDm: Boolean = false,
|
||||||
userHasPermissionToSendMessage: Boolean = true,
|
userHasPermissionToSendMessage: Boolean = true,
|
||||||
) = TimelineRoomInfo(
|
) = TimelineRoomInfo(
|
||||||
isDm = isDm,
|
isDm = isDm,
|
||||||
|
name = name,
|
||||||
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
|
||||||
userHasPermissionToSendReaction = true,
|
userHasPermissionToSendReaction = true,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,6 @@ import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
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.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.components.virtual.TimelineLoadingMoreIndicator
|
||||||
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.aFakeTimelineItemPresenterFactories
|
||||||
|
|
@ -79,7 +78,6 @@ import kotlinx.coroutines.launch
|
||||||
fun TimelineView(
|
fun TimelineView(
|
||||||
state: TimelineState,
|
state: TimelineState,
|
||||||
typingNotificationState: TypingNotificationState,
|
typingNotificationState: TypingNotificationState,
|
||||||
roomName: String?,
|
|
||||||
onUserDataClicked: (UserId) -> Unit,
|
onUserDataClicked: (UserId) -> Unit,
|
||||||
onLinkClicked: (String) -> Unit,
|
onLinkClicked: (String) -> Unit,
|
||||||
onMessageClicked: (TimelineItem.Event) -> Unit,
|
onMessageClicked: (TimelineItem.Event) -> Unit,
|
||||||
|
|
@ -93,9 +91,6 @@ fun TimelineView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
forceJumpToBottomVisibility: Boolean = false
|
forceJumpToBottomVisibility: Boolean = false
|
||||||
) {
|
) {
|
||||||
fun onReachedLoadMore() {
|
|
||||||
state.eventSink(TimelineEvents.LoadMore)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onScrollFinishedAt(firstVisibleIndex: Int) {
|
fun onScrollFinishedAt(firstVisibleIndex: Int) {
|
||||||
state.eventSink(TimelineEvents.OnScrollFinished(firstVisibleIndex))
|
state.eventSink(TimelineEvents.OnScrollFinished(firstVisibleIndex))
|
||||||
|
|
@ -152,20 +147,6 @@ fun TimelineView(
|
||||||
onSwipeToReply = onSwipeToReply,
|
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(
|
TimelineScrollHelper(
|
||||||
|
|
@ -272,17 +253,16 @@ internal fun TimelineViewPreview(
|
||||||
) {
|
) {
|
||||||
TimelineView(
|
TimelineView(
|
||||||
state = aTimelineState(timelineItems),
|
state = aTimelineState(timelineItems),
|
||||||
roomName = null,
|
|
||||||
typingNotificationState = aTypingNotificationState(),
|
typingNotificationState = aTypingNotificationState(),
|
||||||
onMessageClicked = {},
|
|
||||||
onTimestampClicked = {},
|
|
||||||
onUserDataClicked = {},
|
onUserDataClicked = {},
|
||||||
onLinkClicked = {},
|
onLinkClicked = {},
|
||||||
|
onMessageClicked = {},
|
||||||
onMessageLongClicked = {},
|
onMessageLongClicked = {},
|
||||||
|
onTimestampClicked = {},
|
||||||
|
onSwipeToReply = {},
|
||||||
onReactionClicked = { _, _ -> },
|
onReactionClicked = { _, _ -> },
|
||||||
onReactionLongClicked = { _, _ -> },
|
onReactionLongClicked = { _, _ -> },
|
||||||
onMoreReactionsClicked = {},
|
onMoreReactionsClicked = {},
|
||||||
onSwipeToReply = {},
|
|
||||||
onReadReceiptClick = {},
|
onReadReceiptClick = {},
|
||||||
forceJumpToBottomVisibility = true,
|
forceJumpToBottomVisibility = true,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,8 @@ internal fun TimelineItemRow(
|
||||||
is TimelineItem.Virtual -> {
|
is TimelineItem.Virtual -> {
|
||||||
TimelineItemVirtualRow(
|
TimelineItemVirtualRow(
|
||||||
virtual = timelineItem,
|
virtual = timelineItem,
|
||||||
|
timelineRoomInfo = timelineRoomInfo,
|
||||||
|
eventSink = eventSink,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,23 +17,39 @@
|
||||||
package io.element.android.features.messages.impl.timeline.components
|
package io.element.android.features.messages.impl.timeline.components
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Modifier
|
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.TimelineEncryptedHistoryBannerView
|
||||||
import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView
|
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.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.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
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.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.TimelineItemReadMarkerModel
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemRoomBeginningModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemVirtualRow(
|
fun TimelineItemVirtualRow(
|
||||||
virtual: TimelineItem.Virtual,
|
virtual: TimelineItem.Virtual,
|
||||||
|
timelineRoomInfo: TimelineRoomInfo,
|
||||||
|
eventSink: (TimelineEvents.EventFromTimelineItem) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
when (virtual.model) {
|
when (virtual.model) {
|
||||||
is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier)
|
is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier)
|
||||||
TimelineItemReadMarkerModel -> TimelineItemReadMarkerView()
|
TimelineItemReadMarkerModel -> TimelineItemReadMarkerView()
|
||||||
is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(modifier)
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel
|
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.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.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
|
||||||
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.virtual.VirtualTimelineItem
|
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.DayDivider -> daySeparatorFactory.create(inner)
|
||||||
is VirtualTimelineItem.ReadMarker -> TimelineItemReadMarkerModel
|
is VirtualTimelineItem.ReadMarker -> TimelineItemReadMarkerModel
|
||||||
is VirtualTimelineItem.EncryptedHistoryBanner -> TimelineItemEncryptedHistoryBannerVirtualModel
|
is VirtualTimelineItem.EncryptedHistoryBanner -> TimelineItemEncryptedHistoryBannerVirtualModel
|
||||||
|
is VirtualTimelineItem.RoomBeginning -> TimelineItemRoomBeginningModel
|
||||||
|
is VirtualTimelineItem.LoadingIndicator -> TimelineItemLoadingIndicatorModel(
|
||||||
|
backwards = inner.backwards,
|
||||||
|
timestamp = inner.timestamp
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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.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.room.MatrixRoomMembersState
|
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.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.item.event.EventReaction
|
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
|
||||||
|
|
@ -96,15 +95,15 @@ class TimelinePresenterTest {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.paginationState.hasMoreToLoadBackwards).isTrue()
|
assertThat(initialState.backPaginationStatus.hasMoreToLoadBackwards).isTrue()
|
||||||
assertThat(initialState.paginationState.isBackPaginating).isFalse()
|
assertThat(initialState.backPaginationStatus.isBackPaginating).isFalse()
|
||||||
initialState.eventSink.invoke(TimelineEvents.LoadMore)
|
initialState.eventSink.invoke(TimelineEvents.LoadMore)
|
||||||
val inPaginationState = awaitItem()
|
val inPaginationState = awaitItem()
|
||||||
assertThat(inPaginationState.paginationState.isBackPaginating).isTrue()
|
assertThat(inPaginationState.backPaginationStatus.isBackPaginating).isTrue()
|
||||||
assertThat(inPaginationState.paginationState.hasMoreToLoadBackwards).isTrue()
|
assertThat(inPaginationState.backPaginationStatus.hasMoreToLoadBackwards).isTrue()
|
||||||
val postPaginationState = awaitItem()
|
val postPaginationState = awaitItem()
|
||||||
assertThat(postPaginationState.paginationState.hasMoreToLoadBackwards).isTrue()
|
assertThat(postPaginationState.backPaginationStatus.hasMoreToLoadBackwards).isTrue()
|
||||||
assertThat(postPaginationState.paginationState.isBackPaginating).isFalse()
|
assertThat(postPaginationState.backPaginationStatus.isBackPaginating).isFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ class TimelineViewTest {
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
typingNotificationState = aTypingNotificationState(),
|
typingNotificationState = aTypingNotificationState(),
|
||||||
roomName = null,
|
|
||||||
onUserDataClicked = EnsureNeverCalledWithParam(),
|
onUserDataClicked = EnsureNeverCalledWithParam(),
|
||||||
onLinkClicked = EnsureNeverCalledWithParam(),
|
onLinkClicked = EnsureNeverCalledWithParam(),
|
||||||
onMessageClicked = EnsureNeverCalledWithParam(),
|
onMessageClicked = EnsureNeverCalledWithParam(),
|
||||||
|
|
@ -71,7 +70,6 @@ class TimelineViewTest {
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
typingNotificationState = aTypingNotificationState(),
|
typingNotificationState = aTypingNotificationState(),
|
||||||
roomName = null,
|
|
||||||
onUserDataClicked = EnsureNeverCalledWithParam(),
|
onUserDataClicked = EnsureNeverCalledWithParam(),
|
||||||
onLinkClicked = EnsureNeverCalledWithParam(),
|
onLinkClicked = EnsureNeverCalledWithParam(),
|
||||||
onMessageClicked = EnsureNeverCalledWithParam(),
|
onMessageClicked = EnsureNeverCalledWithParam(),
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ class PollRepository @Inject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
) {
|
) {
|
||||||
suspend fun getPoll(eventId: EventId): Result<PollContent> = runCatching {
|
suspend fun getPoll(eventId: EventId): Result<PollContent> = runCatching {
|
||||||
room.timeline
|
room.liveTimeline
|
||||||
.timelineItems
|
.timelineItems
|
||||||
.first()
|
.first()
|
||||||
.asSequence()
|
.asSequence()
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ import io.element.android.features.poll.impl.history.model.PollHistoryItems
|
||||||
import io.element.android.features.poll.impl.history.model.PollHistoryItemsFactory
|
import io.element.android.features.poll.impl.history.model.PollHistoryItemsFactory
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
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.MatrixTimeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -49,8 +49,8 @@ class PollHistoryPresenter @Inject constructor(
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): PollHistoryState {
|
override fun present(): PollHistoryState {
|
||||||
// TODO use room.rememberPollHistory() when working properly?
|
// TODO use room.rememberPollHistory() when working properly?
|
||||||
val timeline = room.timeline
|
val timeline = room.liveTimeline
|
||||||
val paginationState by timeline.paginationState.collectAsState()
|
val paginationState by timeline.backPaginationStatus.collectAsState()
|
||||||
val pollHistoryItemsFlow = remember {
|
val pollHistoryItemsFlow = remember {
|
||||||
timeline.timelineItems.map { items ->
|
timeline.timelineItems.map { items ->
|
||||||
pollHistoryItemFactory.create(items)
|
pollHistoryItemFactory.create(items)
|
||||||
|
|
@ -61,11 +61,11 @@ class PollHistoryPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
val pollHistoryItems by pollHistoryItemsFlow.collectAsState(initial = PollHistoryItems())
|
val pollHistoryItems by pollHistoryItemsFlow.collectAsState(initial = PollHistoryItems())
|
||||||
LaunchedEffect(paginationState, pollHistoryItems.size) {
|
LaunchedEffect(paginationState, pollHistoryItems.size) {
|
||||||
if (pollHistoryItems.size == 0 && paginationState.canBackPaginate) loadMore(timeline)
|
if (pollHistoryItems.size == 0 && paginationState.canPaginate) loadMore(timeline)
|
||||||
}
|
}
|
||||||
val isLoading by remember {
|
val isLoading by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
pollHistoryItems.size == 0 || paginationState.isBackPaginating
|
pollHistoryItems.size == 0 || paginationState.isPaginating
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
@ -88,14 +88,14 @@ class PollHistoryPresenter @Inject constructor(
|
||||||
|
|
||||||
return PollHistoryState(
|
return PollHistoryState(
|
||||||
isLoading = isLoading,
|
isLoading = isLoading,
|
||||||
hasMoreToLoad = paginationState.hasMoreToLoadBackwards,
|
hasMoreToLoad = paginationState.hasMoreToLoad,
|
||||||
pollHistoryItems = pollHistoryItems,
|
pollHistoryItems = pollHistoryItems,
|
||||||
activeFilter = activeFilter,
|
activeFilter = activeFilter,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.loadMore(pollHistory: MatrixTimeline) = launch {
|
private fun CoroutineScope.loadMore(pollHistory: Timeline) = launch {
|
||||||
pollHistory.paginateBackwards(200)
|
pollHistory.paginateBackwards()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import io.element.android.libraries.matrix.api.poll.PollKind
|
||||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
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.MatrixTimeline
|
import io.element.android.libraries.matrix.api.timeline.LiveTimeline
|
||||||
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.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
|
||||||
|
|
@ -97,7 +97,7 @@ interface MatrixRoom : Closeable {
|
||||||
|
|
||||||
val syncUpdateFlow: StateFlow<Long>
|
val syncUpdateFlow: StateFlow<Long>
|
||||||
|
|
||||||
val timeline: MatrixTimeline
|
val liveTimeline: LiveTimeline
|
||||||
|
|
||||||
fun destroy()
|
fun destroy()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.libraries.matrix.api.timeline
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
interface DetachedTimeline : Timeline {
|
||||||
|
suspend fun paginateForwards(): Result<Boolean>
|
||||||
|
val forwardPaginationStatus: StateFlow<Timeline.PaginationStatus>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.matrix.api.timeline
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface LiveTimeline: Timeline {
|
||||||
|
val membershipChangeEventReceived: Flow<Unit>
|
||||||
|
suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result<Unit>
|
||||||
|
}
|
||||||
|
|
@ -1,48 +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.api.timeline
|
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
|
|
||||||
interface MatrixTimeline : AutoCloseable {
|
|
||||||
data class PaginationState(
|
|
||||||
val isBackPaginating: Boolean,
|
|
||||||
val hasMoreToLoadBackwards: Boolean,
|
|
||||||
val beginningOfRoomReached: Boolean,
|
|
||||||
) {
|
|
||||||
val canBackPaginate = !isBackPaginating && hasMoreToLoadBackwards
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val Initial = PaginationState(
|
|
||||||
isBackPaginating = false,
|
|
||||||
hasMoreToLoadBackwards = true,
|
|
||||||
beginningOfRoomReached = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val paginationState: StateFlow<PaginationState>
|
|
||||||
val timelineItems: Flow<List<MatrixTimelineItem>>
|
|
||||||
val membershipChangeEventReceived: Flow<Unit>
|
|
||||||
|
|
||||||
suspend fun paginateBackwards(requestSize: Int): Result<Unit>
|
|
||||||
suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit>
|
|
||||||
suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit>
|
|
||||||
suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result<Unit>
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.matrix.api.timeline
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
interface Timeline : AutoCloseable {
|
||||||
|
|
||||||
|
data class PaginationStatus(
|
||||||
|
val isPaginating: Boolean,
|
||||||
|
val hasMoreToLoad: Boolean,
|
||||||
|
) {
|
||||||
|
val canPaginate: Boolean = !isPaginating && hasMoreToLoad
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun paginateBackwards(): Result<Boolean>
|
||||||
|
val backPaginationStatus: StateFlow<PaginationStatus>
|
||||||
|
val timelineItems: Flow<List<MatrixTimelineItem>>
|
||||||
|
}
|
||||||
|
|
@ -24,4 +24,11 @@ sealed interface VirtualTimelineItem {
|
||||||
data object ReadMarker : VirtualTimelineItem
|
data object ReadMarker : VirtualTimelineItem
|
||||||
|
|
||||||
data object EncryptedHistoryBanner : VirtualTimelineItem
|
data object EncryptedHistoryBanner : VirtualTimelineItem
|
||||||
|
|
||||||
|
data object RoomBeginning: VirtualTimelineItem
|
||||||
|
|
||||||
|
data class LoadingIndicator(
|
||||||
|
val backwards: Boolean,
|
||||||
|
val timestamp: Long,
|
||||||
|
): VirtualTimelineItem
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,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.room.roomNotificationSettings
|
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
import io.element.android.libraries.matrix.api.timeline.LiveTimeline
|
||||||
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.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
|
||||||
|
|
@ -56,7 +56,7 @@ import io.element.android.libraries.matrix.impl.room.location.toInner
|
||||||
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher
|
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher
|
||||||
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
|
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
|
||||||
import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsMapper
|
import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsMapper
|
||||||
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
|
import io.element.android.libraries.matrix.impl.timeline.RustLiveTimeline
|
||||||
import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType
|
import io.element.android.libraries.matrix.impl.timeline.toRustReceiptType
|
||||||
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
|
||||||
import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver
|
import io.element.android.libraries.matrix.impl.widget.RustWidgetDriver
|
||||||
|
|
@ -159,7 +159,7 @@ class RustMatrixRoom(
|
||||||
private val _roomNotificationSettingsStateFlow = MutableStateFlow<MatrixRoomNotificationSettingsState>(MatrixRoomNotificationSettingsState.Unknown)
|
private val _roomNotificationSettingsStateFlow = MutableStateFlow<MatrixRoomNotificationSettingsState>(MatrixRoomNotificationSettingsState.Unknown)
|
||||||
override val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState> = _roomNotificationSettingsStateFlow
|
override val roomNotificationSettingsStateFlow: StateFlow<MatrixRoomNotificationSettingsState> = _roomNotificationSettingsStateFlow
|
||||||
|
|
||||||
override val timeline = createMatrixTimeline(innerTimeline) {
|
override val liveTimeline = createLiveTimeline(innerTimeline){
|
||||||
_syncUpdateFlow.value = systemClock.epochMillis()
|
_syncUpdateFlow.value = systemClock.epochMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,7 +169,7 @@ class RustMatrixRoom(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val powerLevelChanges = roomInfoFlow.map { it.userPowerLevels }.distinctUntilChanged()
|
val powerLevelChanges = roomInfoFlow.map { it.userPowerLevels }.distinctUntilChanged()
|
||||||
val membershipChanges = timeline.membershipChangeEventReceived.onStart { emit(Unit) }
|
val membershipChanges = liveTimeline.membershipChangeEventReceived.onStart { emit(Unit) }
|
||||||
combine(membershipChanges, powerLevelChanges) { _, _ -> }
|
combine(membershipChanges, powerLevelChanges) { _, _ -> }
|
||||||
// Skip initial one
|
// Skip initial one
|
||||||
.drop(1)
|
.drop(1)
|
||||||
|
|
@ -184,7 +184,7 @@ class RustMatrixRoom(
|
||||||
|
|
||||||
override fun destroy() {
|
override fun destroy() {
|
||||||
roomCoroutineScope.cancel()
|
roomCoroutineScope.cancel()
|
||||||
timeline.close()
|
liveTimeline.close()
|
||||||
innerRoom.destroy()
|
innerRoom.destroy()
|
||||||
roomListItem.destroy()
|
roomListItem.destroy()
|
||||||
specialModeEventTimelineItem?.destroy()
|
specialModeEventTimelineItem?.destroy()
|
||||||
|
|
@ -744,18 +744,24 @@ class RustMatrixRoom(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createMatrixTimeline(
|
private fun createLiveTimeline(
|
||||||
timeline: InnerTimeline,
|
timeline: InnerTimeline,
|
||||||
onNewSyncedEvent: () -> Unit = {},
|
onNewSyncedEvent: () -> Unit = {},
|
||||||
): MatrixTimeline {
|
): LiveTimeline {
|
||||||
return RustMatrixTimeline(
|
return RustLiveTimeline(
|
||||||
isKeyBackupEnabled = isKeyBackupEnabled,
|
isKeyBackupEnabled = isKeyBackupEnabled,
|
||||||
matrixRoom = this,
|
matrixRoom = this,
|
||||||
|
systemClock = systemClock,
|
||||||
roomCoroutineScope = roomCoroutineScope,
|
roomCoroutineScope = roomCoroutineScope,
|
||||||
dispatcher = roomDispatcher,
|
dispatcher = roomDispatcher,
|
||||||
lastLoginTimestamp = sessionData.loginTimestamp,
|
lastLoginTimestamp = sessionData.loginTimestamp,
|
||||||
onNewSyncedEvent = onNewSyncedEvent,
|
onNewSyncedEvent = onNewSyncedEvent,
|
||||||
innerTimeline = timeline,
|
inner = timeline,
|
||||||
|
fetchDetailsForEvent = { eventId ->
|
||||||
|
runCatching {
|
||||||
|
innerTimeline.getEventTimelineItemByEventId(eventId.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,106 +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.impl.timeline
|
|
||||||
|
|
||||||
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.MatrixTimelineItem
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.CoroutineStart
|
|
||||||
import kotlinx.coroutines.NonCancellable
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is a wrapper around a [MatrixTimeline] that will be created asynchronously.
|
|
||||||
*/
|
|
||||||
@Suppress("unused")
|
|
||||||
class AsyncMatrixTimeline(
|
|
||||||
coroutineScope: CoroutineScope,
|
|
||||||
dispatcher: CoroutineDispatcher,
|
|
||||||
private val timelineProvider: suspend () -> MatrixTimeline
|
|
||||||
) : MatrixTimeline {
|
|
||||||
private val _timelineItems: MutableStateFlow<List<MatrixTimelineItem>> =
|
|
||||||
MutableStateFlow(emptyList())
|
|
||||||
|
|
||||||
private val _paginationState = MutableStateFlow(
|
|
||||||
MatrixTimeline.PaginationState.Initial
|
|
||||||
)
|
|
||||||
private val timeline = coroutineScope.async(context = dispatcher, start = CoroutineStart.LAZY) {
|
|
||||||
timelineProvider()
|
|
||||||
}
|
|
||||||
private val closeSignal = CompletableDeferred<Unit>()
|
|
||||||
|
|
||||||
override val membershipChangeEventReceived = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
|
||||||
|
|
||||||
init {
|
|
||||||
coroutineScope.launch {
|
|
||||||
val delegateTimeline = timeline.await()
|
|
||||||
delegateTimeline.timelineItems
|
|
||||||
.onEach { _timelineItems.value = it }
|
|
||||||
.launchIn(this)
|
|
||||||
delegateTimeline.paginationState
|
|
||||||
.onEach { _paginationState.value = it }
|
|
||||||
.launchIn(this)
|
|
||||||
delegateTimeline.membershipChangeEventReceived
|
|
||||||
.onEach { membershipChangeEventReceived.emit(it) }
|
|
||||||
.launchIn(this)
|
|
||||||
|
|
||||||
launch {
|
|
||||||
withContext(NonCancellable) {
|
|
||||||
closeSignal.await()
|
|
||||||
Timber.d("Close delegate")
|
|
||||||
delegateTimeline.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val paginationState: StateFlow<MatrixTimeline.PaginationState> = _paginationState
|
|
||||||
override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems
|
|
||||||
|
|
||||||
override suspend fun paginateBackwards(requestSize: Int): Result<Unit> {
|
|
||||||
return timeline.await().paginateBackwards(requestSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> {
|
|
||||||
return timeline.await().paginateBackwards(requestSize, untilNumberOfItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> {
|
|
||||||
return timeline.await().fetchDetailsForEvent(eventId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result<Unit> {
|
|
||||||
return timeline.await().sendReadReceipt(eventId, receiptType)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
closeSignal.complete(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.matrix.impl.timeline
|
||||||
|
|
||||||
|
class RustDetachedTimeline {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.matrix.impl.timeline
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.LiveTimeline
|
||||||
|
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.TimelineException
|
||||||
|
import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
|
||||||
|
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
|
||||||
|
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
|
||||||
|
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
|
||||||
|
import io.element.android.libraries.matrix.impl.timeline.postprocessor.LoadingIndicatorsPostProcessor
|
||||||
|
import io.element.android.libraries.matrix.impl.timeline.postprocessor.RoomBeginningPostProcessor
|
||||||
|
import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor
|
||||||
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.ensureActive
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.matrix.rustcomponents.sdk.TimelineDiff
|
||||||
|
import org.matrix.rustcomponents.sdk.TimelineItem
|
||||||
|
import timber.log.Timber
|
||||||
|
import uniffi.matrix_sdk_ui.EventItemOrigin
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
|
||||||
|
|
||||||
|
private const val INITIAL_MAX_SIZE = 50
|
||||||
|
|
||||||
|
class RustLiveTimeline(
|
||||||
|
private val inner: InnerTimeline,
|
||||||
|
private val systemClock: SystemClock,
|
||||||
|
private val roomCoroutineScope: CoroutineScope,
|
||||||
|
private val isKeyBackupEnabled: Boolean,
|
||||||
|
private val matrixRoom: MatrixRoom,
|
||||||
|
private val dispatcher: CoroutineDispatcher,
|
||||||
|
private val lastLoginTimestamp: Date?,
|
||||||
|
private val fetchDetailsForEvent: suspend (EventId) -> Result<Unit>,
|
||||||
|
private val onNewSyncedEvent: () -> Unit,
|
||||||
|
) : LiveTimeline {
|
||||||
|
|
||||||
|
private val initLatch = CompletableDeferred<Unit>()
|
||||||
|
private val isInit = AtomicBoolean(false)
|
||||||
|
|
||||||
|
private val _timelineItems: MutableStateFlow<List<MatrixTimelineItem>> =
|
||||||
|
MutableStateFlow(emptyList())
|
||||||
|
|
||||||
|
private val encryptedHistoryPostProcessor = TimelineEncryptedHistoryPostProcessor(
|
||||||
|
lastLoginTimestamp = lastLoginTimestamp,
|
||||||
|
isRoomEncrypted = matrixRoom.isEncrypted,
|
||||||
|
isKeyBackupEnabled = isKeyBackupEnabled,
|
||||||
|
dispatcher = dispatcher,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val roomBeginningPostProcessor = RoomBeginningPostProcessor()
|
||||||
|
private val loadingIndicatorsPostProcessor = LoadingIndicatorsPostProcessor(systemClock)
|
||||||
|
|
||||||
|
private val timelineItemFactory = MatrixTimelineItemMapper(
|
||||||
|
fetchDetailsForEvent = fetchDetailsForEvent,
|
||||||
|
roomCoroutineScope = roomCoroutineScope,
|
||||||
|
virtualTimelineItemMapper = VirtualTimelineItemMapper(),
|
||||||
|
eventTimelineItemMapper = EventTimelineItemMapper(
|
||||||
|
contentMapper = TimelineEventContentMapper(
|
||||||
|
eventMessageMapper = EventMessageMapper()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private val timelineDiffProcessor = MatrixTimelineDiffProcessor(
|
||||||
|
timelineItems = _timelineItems,
|
||||||
|
timelineItemFactory = timelineItemFactory,
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
roomCoroutineScope.launch(dispatcher) {
|
||||||
|
inner.timelineDiffFlow { initialList ->
|
||||||
|
postItems(initialList)
|
||||||
|
}.onEach { diffs ->
|
||||||
|
if (diffs.any { diff -> diff.eventOrigin() == EventItemOrigin.SYNC }) {
|
||||||
|
onNewSyncedEvent()
|
||||||
|
}
|
||||||
|
postDiffs(diffs)
|
||||||
|
}.launchIn(this)
|
||||||
|
|
||||||
|
launch {
|
||||||
|
fetchMembers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val membershipChangeEventReceived: Flow<Unit> = timelineDiffProcessor.membershipChangeEventReceived
|
||||||
|
|
||||||
|
override suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result<Unit> {
|
||||||
|
return runCatching {
|
||||||
|
inner.sendReadReceipt(receiptType.toRustReceiptType(), eventId.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun paginateBackwards(): Result<Boolean> {
|
||||||
|
initLatch.await()
|
||||||
|
return runCatching {
|
||||||
|
if (!canBackPaginate()) throw TimelineException.CannotPaginate
|
||||||
|
inner.paginateBackwards()
|
||||||
|
}.onFailure { error ->
|
||||||
|
if (error is TimelineException.CannotPaginate) {
|
||||||
|
Timber.d("Can't paginate backwards on room ${matrixRoom.roomId} with backPaginationStatus: ${backPaginationStatus.value}")
|
||||||
|
} else {
|
||||||
|
Timber.e(error, "Error paginating backwards on room ${matrixRoom.roomId}")
|
||||||
|
}
|
||||||
|
}.onSuccess {
|
||||||
|
Timber.v("Success back paginating for room ${matrixRoom.roomId}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun canBackPaginate(): Boolean {
|
||||||
|
return isInit.get() && backPaginationStatus.value.canPaginate
|
||||||
|
}
|
||||||
|
|
||||||
|
override val backPaginationStatus: StateFlow<Timeline.PaginationStatus> = inner
|
||||||
|
.backPaginationStatusFlow()
|
||||||
|
.map()
|
||||||
|
.stateIn(roomCoroutineScope, SharingStarted.Eagerly, Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true))
|
||||||
|
|
||||||
|
override val timelineItems: Flow<List<MatrixTimelineItem>> = combine(
|
||||||
|
_timelineItems,
|
||||||
|
backPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged()
|
||||||
|
) { timelineItems, hasMoreToLoadBackward ->
|
||||||
|
timelineItems
|
||||||
|
.let { items -> encryptedHistoryPostProcessor.process(items) }
|
||||||
|
.let { items ->
|
||||||
|
roomBeginningPostProcessor.process(
|
||||||
|
items = items,
|
||||||
|
isDm = matrixRoom.isDm,
|
||||||
|
hasMoreToLoadBackwards = hasMoreToLoadBackward
|
||||||
|
)
|
||||||
|
}.let {items -> loadingIndicatorsPostProcessor.process(items, hasMoreToLoadBackward)}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
inner.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchMembers() = withContext(dispatcher) {
|
||||||
|
initLatch.await()
|
||||||
|
try {
|
||||||
|
inner.fetchMembers()
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Timber.e(exception, "Error fetching members for room ${matrixRoom.roomId}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun postItems(items: List<TimelineItem>) = coroutineScope {
|
||||||
|
// Split the initial items in multiple list as there is no pagination in the cached data, so we can post timelineItems asap.
|
||||||
|
items.chunked(INITIAL_MAX_SIZE).reversed().forEach {
|
||||||
|
ensureActive()
|
||||||
|
timelineDiffProcessor.postItems(it)
|
||||||
|
}
|
||||||
|
isInit.set(true)
|
||||||
|
initLatch.complete(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun postDiffs(diffs: List<TimelineDiff>) {
|
||||||
|
initLatch.await()
|
||||||
|
timelineDiffProcessor.postDiffs(diffs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,16 +18,14 @@ package io.element.android.libraries.matrix.impl.timeline
|
||||||
|
|
||||||
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.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
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.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.TimelineException
|
import io.element.android.libraries.matrix.api.timeline.TimelineException
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
|
||||||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
|
import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper
|
||||||
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
|
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
|
||||||
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
|
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
|
||||||
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
|
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
|
||||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.DmBeginningTimelineProcessor
|
import io.element.android.libraries.matrix.impl.timeline.postprocessor.RoomBeginningPostProcessor
|
||||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor
|
import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
|
@ -39,7 +37,6 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
@ -50,224 +47,9 @@ import org.matrix.rustcomponents.sdk.Timeline
|
||||||
import org.matrix.rustcomponents.sdk.TimelineDiff
|
import org.matrix.rustcomponents.sdk.TimelineDiff
|
||||||
import org.matrix.rustcomponents.sdk.TimelineItem
|
import org.matrix.rustcomponents.sdk.TimelineItem
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uniffi.matrix_sdk_ui.BackPaginationStatus
|
|
||||||
import uniffi.matrix_sdk_ui.EventItemOrigin
|
import uniffi.matrix_sdk_ui.EventItemOrigin
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
private const val INITIAL_MAX_SIZE = 50
|
private const val INITIAL_MAX_SIZE = 50
|
||||||
|
|
||||||
class RustMatrixTimeline(
|
|
||||||
roomCoroutineScope: CoroutineScope,
|
|
||||||
isKeyBackupEnabled: Boolean,
|
|
||||||
private val matrixRoom: MatrixRoom,
|
|
||||||
private val innerTimeline: Timeline,
|
|
||||||
private val dispatcher: CoroutineDispatcher,
|
|
||||||
lastLoginTimestamp: Date?,
|
|
||||||
private val onNewSyncedEvent: () -> Unit,
|
|
||||||
) : MatrixTimeline {
|
|
||||||
private val initLatch = CompletableDeferred<Unit>()
|
|
||||||
private val isInit = AtomicBoolean(false)
|
|
||||||
|
|
||||||
private val _timelineItems: MutableStateFlow<List<MatrixTimelineItem>> =
|
|
||||||
MutableStateFlow(emptyList())
|
|
||||||
|
|
||||||
private val _paginationState = MutableStateFlow(
|
|
||||||
MatrixTimeline.PaginationState.Initial
|
|
||||||
)
|
|
||||||
|
|
||||||
private val encryptedHistoryPostProcessor = TimelineEncryptedHistoryPostProcessor(
|
|
||||||
lastLoginTimestamp = lastLoginTimestamp,
|
|
||||||
isRoomEncrypted = matrixRoom.isEncrypted,
|
|
||||||
isKeyBackupEnabled = isKeyBackupEnabled,
|
|
||||||
dispatcher = dispatcher,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val dmBeginningTimelineProcessor = DmBeginningTimelineProcessor()
|
|
||||||
|
|
||||||
private val timelineItemFactory = MatrixTimelineItemMapper(
|
|
||||||
fetchDetailsForEvent = this::fetchDetailsForEvent,
|
|
||||||
roomCoroutineScope = roomCoroutineScope,
|
|
||||||
virtualTimelineItemMapper = VirtualTimelineItemMapper(),
|
|
||||||
eventTimelineItemMapper = EventTimelineItemMapper(
|
|
||||||
contentMapper = TimelineEventContentMapper(
|
|
||||||
eventMessageMapper = EventMessageMapper()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
private val timelineDiffProcessor = MatrixTimelineDiffProcessor(
|
|
||||||
timelineItems = _timelineItems,
|
|
||||||
timelineItemFactory = timelineItemFactory,
|
|
||||||
)
|
|
||||||
|
|
||||||
override val paginationState: StateFlow<MatrixTimeline.PaginationState> = _paginationState.asStateFlow()
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems
|
|
||||||
.mapLatest { items -> encryptedHistoryPostProcessor.process(items) }
|
|
||||||
.mapLatest { items ->
|
|
||||||
dmBeginningTimelineProcessor.process(
|
|
||||||
items = items,
|
|
||||||
isDm = matrixRoom.isDirect && matrixRoom.isOneToOne,
|
|
||||||
isAtStartOfTimeline = paginationState.value.beginningOfRoomReached
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val membershipChangeEventReceived: Flow<Unit> = timelineDiffProcessor.membershipChangeEventReceived
|
|
||||||
|
|
||||||
init {
|
|
||||||
Timber.d("Initialize timeline for room ${matrixRoom.roomId}")
|
|
||||||
|
|
||||||
roomCoroutineScope.launch(dispatcher) {
|
|
||||||
innerTimeline.timelineDiffFlow { initialList ->
|
|
||||||
postItems(initialList)
|
|
||||||
}.onEach { diffs ->
|
|
||||||
if (diffs.any { diff -> diff.eventOrigin() == EventItemOrigin.SYNC }) {
|
|
||||||
onNewSyncedEvent()
|
|
||||||
}
|
|
||||||
postDiffs(diffs)
|
|
||||||
}.launchIn(this)
|
|
||||||
|
|
||||||
paginationStateFlow()
|
|
||||||
.onEach {
|
|
||||||
_paginationState.value = it
|
|
||||||
}
|
|
||||||
.launchIn(this)
|
|
||||||
|
|
||||||
fetchMembers()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun paginationStateFlow(): Flow<MatrixTimeline.PaginationState> {
|
|
||||||
return combine(
|
|
||||||
innerTimeline.backPaginationStatusFlow(),
|
|
||||||
timelineItems,
|
|
||||||
) { paginationStatus, filteredItems ->
|
|
||||||
if (filteredItems.hasEncryptionHistoryBanner()) {
|
|
||||||
return@combine MatrixTimeline.PaginationState(
|
|
||||||
isBackPaginating = false,
|
|
||||||
hasMoreToLoadBackwards = false,
|
|
||||||
beginningOfRoomReached = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
when (paginationStatus) {
|
|
||||||
BackPaginationStatus.IDLE -> {
|
|
||||||
MatrixTimeline.PaginationState(
|
|
||||||
isBackPaginating = false,
|
|
||||||
hasMoreToLoadBackwards = true,
|
|
||||||
beginningOfRoomReached = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
BackPaginationStatus.PAGINATING -> {
|
|
||||||
MatrixTimeline.PaginationState(
|
|
||||||
isBackPaginating = true,
|
|
||||||
hasMoreToLoadBackwards = true,
|
|
||||||
beginningOfRoomReached = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
BackPaginationStatus.TIMELINE_START_REACHED -> {
|
|
||||||
MatrixTimeline.PaginationState(
|
|
||||||
isBackPaginating = false,
|
|
||||||
hasMoreToLoadBackwards = false,
|
|
||||||
beginningOfRoomReached = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun fetchMembers() = withContext(dispatcher) {
|
|
||||||
initLatch.await()
|
|
||||||
try {
|
|
||||||
innerTimeline.fetchMembers()
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Timber.e(exception, "Error fetching members for room ${matrixRoom.roomId}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun postItems(items: List<TimelineItem>) = coroutineScope {
|
|
||||||
// Split the initial items in multiple list as there is no pagination in the cached data, so we can post timelineItems asap.
|
|
||||||
items.chunked(INITIAL_MAX_SIZE).reversed().forEach {
|
|
||||||
ensureActive()
|
|
||||||
timelineDiffProcessor.postItems(it)
|
|
||||||
}
|
|
||||||
isInit.set(true)
|
|
||||||
initLatch.complete(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun postDiffs(diffs: List<TimelineDiff>) {
|
|
||||||
initLatch.await()
|
|
||||||
timelineDiffProcessor.postDiffs(diffs)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> = withContext(dispatcher) {
|
|
||||||
runCatching {
|
|
||||||
innerTimeline.fetchDetailsForEvent(eventId.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun paginateBackwards(requestSize: Int): Result<Unit> {
|
|
||||||
val paginationOptions = PaginationOptions.SimpleRequest(
|
|
||||||
eventLimit = requestSize.toUShort(),
|
|
||||||
waitForToken = true,
|
|
||||||
)
|
|
||||||
return paginateBackwards(paginationOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> {
|
|
||||||
val paginationOptions = PaginationOptions.UntilNumItems(
|
|
||||||
eventLimit = requestSize.toUShort(),
|
|
||||||
items = untilNumberOfItems.toUShort(),
|
|
||||||
waitForToken = true,
|
|
||||||
)
|
|
||||||
return paginateBackwards(paginationOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun paginateBackwards(paginationOptions: PaginationOptions): Result<Unit> = withContext(dispatcher) {
|
|
||||||
initLatch.await()
|
|
||||||
runCatching {
|
|
||||||
if (!canBackPaginate()) throw TimelineException.CannotPaginate
|
|
||||||
Timber.v("Start back paginating for room ${matrixRoom.roomId} ")
|
|
||||||
innerTimeline.paginateBackwards(paginationOptions)
|
|
||||||
}.onFailure { error ->
|
|
||||||
if (error is TimelineException.CannotPaginate) {
|
|
||||||
Timber.d("Can't paginate backwards on room ${matrixRoom.roomId}, we're already at the start")
|
|
||||||
} else {
|
|
||||||
Timber.e(error, "Error paginating backwards on room ${matrixRoom.roomId}")
|
|
||||||
}
|
|
||||||
}.onSuccess {
|
|
||||||
Timber.v("Success back paginating for room ${matrixRoom.roomId}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun canBackPaginate(): Boolean {
|
|
||||||
return isInit.get() && paginationState.value.canBackPaginate
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun sendReadReceipt(
|
|
||||||
eventId: EventId,
|
|
||||||
receiptType: ReceiptType,
|
|
||||||
) = withContext(dispatcher) {
|
|
||||||
runCatching {
|
|
||||||
innerTimeline.sendReadReceipt(
|
|
||||||
receiptType = receiptType.toRustReceiptType(),
|
|
||||||
eventId = eventId.value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
innerTimeline.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getItemById(eventId: EventId): MatrixTimelineItem.Event? {
|
|
||||||
return _timelineItems.value.firstOrNull { (it as? MatrixTimelineItem.Event)?.eventId == eventId } as? MatrixTimelineItem.Event
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<MatrixTimelineItem>.hasEncryptionHistoryBanner(): Boolean {
|
|
||||||
val firstItem = firstOrNull()
|
|
||||||
return firstItem is MatrixTimelineItem.Virtual &&
|
|
||||||
firstItem.virtual is VirtualTimelineItem.EncryptedHistoryBanner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.matrix.impl.timeline
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import uniffi.matrix_sdk_ui.PaginationStatus
|
||||||
|
|
||||||
|
fun Flow<PaginationStatus>.map(): Flow<Timeline.PaginationStatus> = map { paginationStatus ->
|
||||||
|
when (paginationStatus) {
|
||||||
|
PaginationStatus.IDLE -> Timeline.PaginationStatus(
|
||||||
|
isPaginating = false,
|
||||||
|
hasMoreToLoad = true
|
||||||
|
)
|
||||||
|
PaginationStatus.PAGINATING -> Timeline.PaginationStatus(
|
||||||
|
isPaginating = true,
|
||||||
|
hasMoreToLoad = true
|
||||||
|
)
|
||||||
|
PaginationStatus.TIMELINE_END_REACHED -> Timeline.PaginationStatus(
|
||||||
|
isPaginating = false,
|
||||||
|
hasMoreToLoad = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.matrix.impl.timeline.postprocessor
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
|
|
||||||
|
fun List<MatrixTimelineItem>.hasEncryptionHistoryBanner(): Boolean {
|
||||||
|
val firstItem = firstOrNull()
|
||||||
|
return firstItem is MatrixTimelineItem.Virtual &&
|
||||||
|
firstItem.virtual is VirtualTimelineItem.EncryptedHistoryBanner
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.matrix.impl.timeline.postprocessor
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
|
|
||||||
|
class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) {
|
||||||
|
|
||||||
|
fun process(
|
||||||
|
items: List<MatrixTimelineItem>,
|
||||||
|
hasMoreToLoadBackwards: Boolean,
|
||||||
|
): List<MatrixTimelineItem> {
|
||||||
|
return if (hasMoreToLoadBackwards && !items.hasEncryptionHistoryBanner()){
|
||||||
|
listOf(
|
||||||
|
MatrixTimelineItem.Virtual(
|
||||||
|
uniqueId = "BackwardLoadingIndicator",
|
||||||
|
virtual = VirtualTimelineItem.LoadingIndicator(
|
||||||
|
backwards = true,
|
||||||
|
timestamp = systemClock.epochMillis()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) + items
|
||||||
|
}else {
|
||||||
|
items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -21,17 +21,36 @@ import io.element.android.libraries.matrix.api.timeline.item.event.MembershipCha
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
|
import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This timeline post-processor removes the room creation event and the self-join event from the timeline for DMs.
|
* This timeline post-processor removes the room creation event and the self-join event from the timeline for DMs
|
||||||
|
* or add the RoomBeginning item for non DM room.
|
||||||
*/
|
*/
|
||||||
class DmBeginningTimelineProcessor {
|
class RoomBeginningPostProcessor {
|
||||||
|
|
||||||
fun process(
|
fun process(
|
||||||
items: List<MatrixTimelineItem>,
|
items: List<MatrixTimelineItem>,
|
||||||
isDm: Boolean,
|
isDm: Boolean,
|
||||||
isAtStartOfTimeline: Boolean
|
hasMoreToLoadBackwards: Boolean
|
||||||
): List<MatrixTimelineItem> {
|
): List<MatrixTimelineItem> {
|
||||||
if (!isDm || !isAtStartOfTimeline) return items
|
return when {
|
||||||
|
hasMoreToLoadBackwards -> items
|
||||||
|
isDm -> processForDM(items)
|
||||||
|
else -> processForRoom(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processForRoom(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> {
|
||||||
|
if (items.hasEncryptionHistoryBanner()) return items
|
||||||
|
val roomBeginningItem = MatrixTimelineItem.Virtual(
|
||||||
|
uniqueId = VirtualTimelineItem.RoomBeginning.toString(),
|
||||||
|
virtual = VirtualTimelineItem.RoomBeginning
|
||||||
|
)
|
||||||
|
return listOf(roomBeginningItem) + items
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processForDM(items: List<MatrixTimelineItem>): List<MatrixTimelineItem> {
|
||||||
|
|
||||||
// Find room creation event. This is usually index 0
|
// Find room creation event. This is usually index 0
|
||||||
val roomCreationEventIndex = items.indexOfFirst {
|
val roomCreationEventIndex = items.indexOfFirst {
|
||||||
|
|
@ -35,8 +35,8 @@ class DmBeginningTimelineProcessorTest {
|
||||||
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
||||||
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
|
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
|
||||||
)
|
)
|
||||||
val processor = DmBeginningTimelineProcessor()
|
val processor = RoomBeginningPostProcessor()
|
||||||
val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = true)
|
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
|
||||||
assertThat(processedItems).isEmpty()
|
assertThat(processedItems).isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,8 +52,8 @@ class DmBeginningTimelineProcessorTest {
|
||||||
MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))),
|
MatrixTimelineItem.Event("m.room.member_other", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))),
|
||||||
MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))),
|
MatrixTimelineItem.Event("m.room.message", anEventTimelineItem(content = aMessageContent("hi"))),
|
||||||
)
|
)
|
||||||
val processor = DmBeginningTimelineProcessor()
|
val processor = RoomBeginningPostProcessor()
|
||||||
val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = true)
|
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = true)
|
||||||
assertThat(processedItems).isEqualTo(expected)
|
assertThat(processedItems).isEqualTo(expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,8 +63,8 @@ class DmBeginningTimelineProcessorTest {
|
||||||
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
||||||
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
|
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
|
||||||
)
|
)
|
||||||
val processor = DmBeginningTimelineProcessor()
|
val processor = RoomBeginningPostProcessor()
|
||||||
val processedItems = processor.process(timelineItems, isDm = false, isAtStartOfTimeline = true)
|
val processedItems = processor.process(timelineItems, isDm = false, hasMoreToLoadBackwards = true)
|
||||||
assertThat(processedItems).isEqualTo(timelineItems)
|
assertThat(processedItems).isEqualTo(timelineItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,8 +74,8 @@ class DmBeginningTimelineProcessorTest {
|
||||||
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
||||||
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
|
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
|
||||||
)
|
)
|
||||||
val processor = DmBeginningTimelineProcessor()
|
val processor = RoomBeginningPostProcessor()
|
||||||
val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = false)
|
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false)
|
||||||
assertThat(processedItems).isEqualTo(timelineItems)
|
assertThat(processedItems).isEqualTo(timelineItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,8 +84,8 @@ class DmBeginningTimelineProcessorTest {
|
||||||
val timelineItems = listOf(
|
val timelineItems = listOf(
|
||||||
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
|
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID, MembershipChange.JOINED))),
|
||||||
)
|
)
|
||||||
val processor = DmBeginningTimelineProcessor()
|
val processor = RoomBeginningPostProcessor()
|
||||||
val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = false)
|
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false)
|
||||||
assertThat(processedItems).isEqualTo(timelineItems)
|
assertThat(processedItems).isEqualTo(timelineItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,8 +95,8 @@ class DmBeginningTimelineProcessorTest {
|
||||||
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
MatrixTimelineItem.Event("m.room.create", anEventTimelineItem(sender = A_USER_ID, content = StateContent("", OtherState.RoomCreate))),
|
||||||
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))),
|
MatrixTimelineItem.Event("m.room.member", anEventTimelineItem(content = RoomMembershipContent(A_USER_ID_2, MembershipChange.JOINED))),
|
||||||
)
|
)
|
||||||
val processor = DmBeginningTimelineProcessor()
|
val processor = RoomBeginningPostProcessor()
|
||||||
val processedItems = processor.process(timelineItems, isDm = true, isAtStartOfTimeline = false)
|
val processedItems = processor.process(timelineItems, isDm = true, hasMoreToLoadBackwards = false)
|
||||||
assertThat(processedItems).isEqualTo(timelineItems)
|
assertThat(processedItems).isEqualTo(timelineItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ import io.element.android.libraries.matrix.api.room.StateEventType
|
||||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
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.MatrixTimeline
|
|
||||||
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.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
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
package io.element.android.libraries.matrix.test.timeline
|
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.EventId
|
||||||
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.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue