Timeline permalink : continue to iterate (try a strategy to avoid forward insertion to "auto-scroll")
This commit is contained in:
parent
6808edda86
commit
f56ac869ff
25 changed files with 599 additions and 218 deletions
|
|
@ -30,6 +30,7 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
|
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||||
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.TimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
|
@ -116,6 +117,7 @@ class MessagesNode @AssistedInject constructor(
|
||||||
private fun onLinkClicked(
|
private fun onLinkClicked(
|
||||||
context: Context,
|
context: Context,
|
||||||
url: String,
|
url: String,
|
||||||
|
eventSink: (TimelineEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
when (val permalink = permalinkParser.parse(url)) {
|
when (val permalink = permalinkParser.parse(url)) {
|
||||||
is PermalinkData.UserLink -> {
|
is PermalinkData.UserLink -> {
|
||||||
|
|
@ -124,7 +126,7 @@ class MessagesNode @AssistedInject constructor(
|
||||||
callback?.onUserDataClicked(permalink.userId)
|
callback?.onUserDataClicked(permalink.userId)
|
||||||
}
|
}
|
||||||
is PermalinkData.RoomLink -> {
|
is PermalinkData.RoomLink -> {
|
||||||
handleRoomLinkClicked(permalink)
|
handleRoomLinkClicked(permalink, eventSink)
|
||||||
}
|
}
|
||||||
is PermalinkData.FallbackLink,
|
is PermalinkData.FallbackLink,
|
||||||
is PermalinkData.RoomEmailInviteLink -> {
|
is PermalinkData.RoomEmailInviteLink -> {
|
||||||
|
|
@ -133,11 +135,11 @@ class MessagesNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleRoomLinkClicked(roomLink: PermalinkData.RoomLink) {
|
private fun handleRoomLinkClicked(roomLink: PermalinkData.RoomLink, eventSink: (TimelineEvents) -> Unit) {
|
||||||
if (room.matches(roomLink.roomIdOrAlias)) {
|
if (room.matches(roomLink.roomIdOrAlias)) {
|
||||||
if (roomLink.eventId != null) {
|
val eventId = roomLink.eventId
|
||||||
// TODO Handle navigation to the Event
|
if (eventId != null) {
|
||||||
context.toast("TODO Handle navigation to the Event ${roomLink.eventId}")
|
eventSink(TimelineEvents.FocusOnEvent(eventId))
|
||||||
} else {
|
} else {
|
||||||
// Click on the same room, ignore
|
// Click on the same room, ignore
|
||||||
context.toast("Already viewing this room!")
|
context.toast("Already viewing this room!")
|
||||||
|
|
@ -189,7 +191,7 @@ class MessagesNode @AssistedInject constructor(
|
||||||
onEventClicked = this::onEventClicked,
|
onEventClicked = this::onEventClicked,
|
||||||
onPreviewAttachments = this::onPreviewAttachments,
|
onPreviewAttachments = this::onPreviewAttachments,
|
||||||
onUserDataClicked = this::onUserDataClicked,
|
onUserDataClicked = this::onUserDataClicked,
|
||||||
onLinkClicked = { onLinkClicked(context, it) },
|
onLinkClicked = { onLinkClicked(context, it, state.timelineState.eventSink) },
|
||||||
onSendLocationClicked = this::onSendLocationClicked,
|
onSendLocationClicked = this::onSendLocationClicked,
|
||||||
onCreatePollClicked = this::onCreatePollClicked,
|
onCreatePollClicked = this::onCreatePollClicked,
|
||||||
onJoinCallClicked = this::onJoinCallClicked,
|
onJoinCallClicked = this::onJoinCallClicked,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
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.MatrixTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.getAndUpdate
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import java.util.Optional
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
@SingleIn(RoomScope::class)
|
||||||
|
class TimelineController @Inject constructor(
|
||||||
|
private val room: MatrixRoom,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val liveTimeline = MutableStateFlow(room.liveTimeline)
|
||||||
|
private val detachedTimeline = MutableStateFlow<Optional<Timeline>>(Optional.empty())
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
fun timelineItems(): Flow<List<MatrixTimelineItem>> {
|
||||||
|
return currentTimelineFlow().flatMapLatest { it.timelineItems }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isLive(): Flow<Boolean> {
|
||||||
|
return detachedTimeline.map { !it.isPresent }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun focusOnEvent(eventId: EventId): Result<Unit> {
|
||||||
|
return try {
|
||||||
|
val newDetachedTimeline = room.timelineFocusedOnEvent(eventId)
|
||||||
|
detachedTimeline.getAndUpdate { current ->
|
||||||
|
if (current.isPresent) {
|
||||||
|
current.get().close()
|
||||||
|
}
|
||||||
|
Optional.of(newDetachedTimeline)
|
||||||
|
}
|
||||||
|
Result.success(Unit)
|
||||||
|
} catch (cancellation: CancellationException) {
|
||||||
|
throw cancellation
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Result.failure(exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes sure the controller is focused on the live timeline.
|
||||||
|
* This does close the detached timeline if any.
|
||||||
|
*/
|
||||||
|
fun focusOnLive() {
|
||||||
|
detachedTimeline.getAndUpdate {
|
||||||
|
when {
|
||||||
|
it.isPresent -> {
|
||||||
|
it.get().close()
|
||||||
|
Optional.empty()
|
||||||
|
}
|
||||||
|
else -> Optional.empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> {
|
||||||
|
return currentTimelineFlow().first().paginate(direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun currentTimelineFlow() = combine(liveTimeline, detachedTimeline) { live, detached ->
|
||||||
|
when {
|
||||||
|
detached.isPresent -> detached.get()
|
||||||
|
else -> live
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sendReadReceiptIfNeeded(
|
||||||
|
firstVisibleIndex: Int,
|
||||||
|
timelineItems: ImmutableList<TimelineItem>,
|
||||||
|
lastReadReceiptId: MutableState<EventId?>,
|
||||||
|
readReceiptType: ReceiptType,
|
||||||
|
) {
|
||||||
|
// If we are at the bottom of timeline, we mark the room as read.
|
||||||
|
if (firstVisibleIndex == 0) {
|
||||||
|
room.markAsRead(receiptType = readReceiptType)
|
||||||
|
} else {
|
||||||
|
// Get last valid EventId seen by the user, as the first index might refer to a Virtual item
|
||||||
|
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
|
||||||
|
if (eventId != null && eventId != lastReadReceiptId.value) {
|
||||||
|
lastReadReceiptId.value = eventId
|
||||||
|
currentTimelineFlow()
|
||||||
|
.filterIsInstance(Timeline::class)
|
||||||
|
.first()
|
||||||
|
.sendReadReceipt(eventId = eventId, receiptType = readReceiptType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLastEventIdBeforeOrAt(index: Int, items: ImmutableList<TimelineItem>): EventId? {
|
||||||
|
for (i in index until items.count()) {
|
||||||
|
val item = items[i]
|
||||||
|
if (item is TimelineItem.Event) {
|
||||||
|
return item.eventId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,17 +17,21 @@
|
||||||
package io.element.android.features.messages.impl.timeline
|
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
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
|
||||||
sealed interface TimelineEvents {
|
sealed interface 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
|
||||||
|
data class FocusOnEvent(val eventId: EventId) : TimelineEvents
|
||||||
|
data object ClearFocusRequestState: TimelineEvents
|
||||||
|
data object JumpToLive : TimelineEvents
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events coming from a timeline item.
|
* Events coming from a timeline item.
|
||||||
*/
|
*/
|
||||||
sealed interface EventFromTimelineItem : TimelineEvents
|
sealed interface EventFromTimelineItem : TimelineEvents
|
||||||
|
|
||||||
data class LoadMore(val backwards: Boolean) : EventFromTimelineItem
|
data class LoadMore(val direction: Timeline.PaginationDirection) : EventFromTimelineItem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events coming from a poll item.
|
* Events coming from a poll item.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@SingleIn(RoomScope::class)
|
||||||
|
class TimelineItemIndexer @Inject constructor() {
|
||||||
|
|
||||||
|
private val timelineEventsIndexes = mutableMapOf<EventId, Int>()
|
||||||
|
|
||||||
|
fun isKnown(eventId: EventId): Boolean {
|
||||||
|
return timelineEventsIndexes.containsKey(eventId).also {
|
||||||
|
Timber.d("$eventId isKnown = $it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun indexOf(eventId: EventId): Int {
|
||||||
|
return (timelineEventsIndexes[eventId] ?: -1).also {
|
||||||
|
Timber.d("indexOf $eventId= $it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun process(timelineItems: List<TimelineItem>) {
|
||||||
|
Timber.d("process ${timelineItems.size} items")
|
||||||
|
timelineEventsIndexes.clear()
|
||||||
|
timelineItems.forEachIndexed { index, timelineItem ->
|
||||||
|
when (timelineItem) {
|
||||||
|
is TimelineItem.Event -> {
|
||||||
|
processEvent(timelineItem, index)
|
||||||
|
}
|
||||||
|
is TimelineItem.GroupedEvents -> {
|
||||||
|
timelineItem.events.forEach { event ->
|
||||||
|
processEvent(event, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processEvent(event: TimelineItem.Event, index: Int) {
|
||||||
|
if (event.eventId == null) return
|
||||||
|
timelineEventsIndexes[event.eventId] = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -56,6 +56,7 @@ import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class TimelinePresenter @AssistedInject constructor(
|
class TimelinePresenter @AssistedInject constructor(
|
||||||
private val timelineItemsFactory: TimelineItemsFactory,
|
private val timelineItemsFactory: TimelineItemsFactory,
|
||||||
|
private val timelineItemIndexer: TimelineItemIndexer,
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val appScope: CoroutineScope,
|
private val appScope: CoroutineScope,
|
||||||
|
|
@ -64,14 +65,13 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
private val sendPollResponseAction: SendPollResponseAction,
|
private val sendPollResponseAction: SendPollResponseAction,
|
||||||
private val endPollAction: EndPollAction,
|
private val endPollAction: EndPollAction,
|
||||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
|
private val timelineController: TimelineController,
|
||||||
) : Presenter<TimelineState> {
|
) : Presenter<TimelineState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(navigator: MessagesNavigator): TimelinePresenter
|
fun create(navigator: MessagesNavigator): TimelinePresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
private val timeline = room.liveTimeline
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): TimelineState {
|
override fun present(): TimelineState {
|
||||||
val localScope = rememberCoroutineScope()
|
val localScope = rememberCoroutineScope()
|
||||||
|
|
@ -79,34 +79,42 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
mutableStateOf(null)
|
mutableStateOf(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val focusedEventId: MutableState<EventId?> = rememberSaveable {
|
||||||
|
mutableStateOf(null)
|
||||||
|
}
|
||||||
|
val focusRequestState: MutableState<FocusRequestState> = remember {
|
||||||
|
mutableStateOf(FocusRequestState.None)
|
||||||
|
}
|
||||||
|
|
||||||
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.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)
|
||||||
val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION, updateKey = syncUpdateFlow.value)
|
val userHasPermissionToSendReaction by room.canSendMessageAsState(type = MessageEventType.REACTION, updateKey = syncUpdateFlow.value)
|
||||||
|
|
||||||
val prevMostRecentItemId = rememberSaveable { mutableStateOf<String?>(null) }
|
val prevMostRecentItemId = rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
val newItemState = remember { mutableStateOf(NewEventState.None) }
|
|
||||||
|
val newEventState = remember { mutableStateOf(NewEventState.None) }
|
||||||
|
|
||||||
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true)
|
val isSendPublicReadReceiptsEnabled by sessionPreferencesStore.isSendPublicReadReceiptsEnabled().collectAsState(initial = true)
|
||||||
val renderReadReceipts by sessionPreferencesStore.isRenderReadReceiptsEnabled().collectAsState(initial = true)
|
val renderReadReceipts by sessionPreferencesStore.isRenderReadReceiptsEnabled().collectAsState(initial = true)
|
||||||
|
val isLive by timelineController.isLive().collectAsState(initial = true)
|
||||||
|
|
||||||
fun handleEvents(event: TimelineEvents) {
|
fun handleEvents(event: TimelineEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is TimelineEvents.LoadMore -> {
|
is TimelineEvents.LoadMore -> {
|
||||||
if(event.backwards) {
|
localScope.launch {
|
||||||
localScope.paginateBackwards()
|
timelineController.paginate(direction = event.direction)
|
||||||
}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 (isLive) {
|
||||||
if (event.firstIndex == 0) {
|
if (event.firstIndex == 0) {
|
||||||
newItemState.value = NewEventState.None
|
newEventState.value = NewEventState.None
|
||||||
}
|
}
|
||||||
appScope.sendReadReceiptIfNeeded(
|
appScope.sendReadReceiptIfNeeded(
|
||||||
firstVisibleIndex = event.firstIndex,
|
firstVisibleIndex = event.firstIndex,
|
||||||
|
|
@ -114,6 +122,9 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
lastReadReceiptId = lastReadReceiptId,
|
lastReadReceiptId = lastReadReceiptId,
|
||||||
readReceiptType = if (isSendPublicReadReceiptsEnabled) ReceiptType.READ else ReceiptType.READ_PRIVATE,
|
readReceiptType = if (isSendPublicReadReceiptsEnabled) ReceiptType.READ else ReceiptType.READ_PRIVATE,
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
newEventState.value = NewEventState.None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is TimelineEvents.PollAnswerSelected -> appScope.launch {
|
is TimelineEvents.PollAnswerSelected -> appScope.launch {
|
||||||
sendPollResponseAction.execute(
|
sendPollResponseAction.execute(
|
||||||
|
|
@ -126,27 +137,54 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
pollStartId = event.pollStartId,
|
pollStartId = event.pollStartId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is TimelineEvents.PollEditClicked ->
|
is TimelineEvents.PollEditClicked -> {
|
||||||
navigator.onEditPollClicked(event.pollStartId)
|
navigator.onEditPollClicked(event.pollStartId)
|
||||||
}
|
}
|
||||||
|
is TimelineEvents.FocusOnEvent -> localScope.launch {
|
||||||
|
focusedEventId.value = event.eventId
|
||||||
|
if (timelineItemIndexer.isKnown(event.eventId)) {
|
||||||
|
val index = timelineItemIndexer.indexOf(event.eventId)
|
||||||
|
focusRequestState.value = FocusRequestState.Cached(index)
|
||||||
|
} else {
|
||||||
|
focusRequestState.value = FocusRequestState.Fetching
|
||||||
|
timelineController.focusOnEvent(event.eventId)
|
||||||
|
.fold(
|
||||||
|
onSuccess = {
|
||||||
|
focusRequestState.value = FocusRequestState.None
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
focusRequestState.value = FocusRequestState.Failure(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TimelineEvents.ClearFocusRequestState -> {
|
||||||
|
focusRequestState.value = FocusRequestState.None
|
||||||
|
}
|
||||||
|
is TimelineEvents.JumpToLive -> {
|
||||||
|
localScope.launch {
|
||||||
|
timelineController.focusOnLive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Makes sure to get back to live when there is nothing more to load forwards
|
||||||
|
LaunchedEffect(isLive) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(timelineItems.size) {
|
LaunchedEffect(timelineItems.size) {
|
||||||
computeNewItemState(timelineItems, prevMostRecentItemId, newItemState)
|
computeNewItemState(timelineItems, prevMostRecentItemId, newEventState)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
combine(timeline.timelineItems, room.membersStateFlow) { items, membersState ->
|
combine(timelineController.timelineItems(), room.membersStateFlow) { items, membersState ->
|
||||||
timelineItemsFactory.replaceWith(
|
timelineItemsFactory.replaceWith(
|
||||||
timelineItems = items,
|
timelineItems = items,
|
||||||
roomMembers = membersState.roomMembers().orEmpty()
|
roomMembers = membersState.roomMembers().orEmpty()
|
||||||
)
|
)
|
||||||
items
|
items
|
||||||
}
|
|
||||||
.onEach { timelineItems ->
|
|
||||||
if (timelineItems.isEmpty()) {
|
|
||||||
paginateBackwards()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onEach(redactedVoiceMessageManager::onEachMatrixTimelineItem)
|
.onEach(redactedVoiceMessageManager::onEachMatrixTimelineItem)
|
||||||
.launchIn(this)
|
.launchIn(this)
|
||||||
|
|
@ -165,10 +203,12 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
return TimelineState(
|
return TimelineState(
|
||||||
timelineRoomInfo = timelineRoomInfo,
|
timelineRoomInfo = timelineRoomInfo,
|
||||||
highlightedEventId = highlightedEventId.value,
|
highlightedEventId = highlightedEventId.value,
|
||||||
backPaginationStatus = paginationState,
|
|
||||||
timelineItems = timelineItems,
|
timelineItems = timelineItems,
|
||||||
renderReadReceipts = renderReadReceipts,
|
renderReadReceipts = renderReadReceipts,
|
||||||
newEventState = newItemState.value,
|
newEventState = newEventState.value,
|
||||||
|
isLive = isLive,
|
||||||
|
focusedEventId = focusedEventId.value,
|
||||||
|
focusRequestState = focusRequestState.value,
|
||||||
eventSink = { handleEvents(it) }
|
eventSink = { handleEvents(it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -194,6 +234,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
newMostRecentItem is TimelineItem.Event &&
|
newMostRecentItem is TimelineItem.Event &&
|
||||||
newMostRecentItem.origin != TimelineItemEventOrigin.PAGINATION &&
|
newMostRecentItem.origin != TimelineItemEventOrigin.PAGINATION &&
|
||||||
newMostRecentItemId != prevMostRecentItemIdValue
|
newMostRecentItemId != prevMostRecentItemIdValue
|
||||||
|
|
||||||
if (hasNewEvent) {
|
if (hasNewEvent) {
|
||||||
val newMostRecentEvent = newMostRecentItem as? TimelineItem.Event
|
val newMostRecentEvent = newMostRecentItem as? TimelineItem.Event
|
||||||
// Scroll to bottom if the new event is from me, even if sent from another device
|
// Scroll to bottom if the new event is from me, even if sent from another device
|
||||||
|
|
@ -221,7 +262,7 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
|
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
|
||||||
if (eventId != null && eventId != lastReadReceiptId.value) {
|
if (eventId != null && eventId != lastReadReceiptId.value) {
|
||||||
lastReadReceiptId.value = eventId
|
lastReadReceiptId.value = eventId
|
||||||
timeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType)
|
//timeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -235,8 +276,4 @@ class TimelinePresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.paginateBackwards() = launch {
|
|
||||||
timeline.paginateBackwards()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ 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.Timeline
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
@ -29,11 +28,20 @@ data class TimelineState(
|
||||||
val timelineRoomInfo: TimelineRoomInfo,
|
val timelineRoomInfo: TimelineRoomInfo,
|
||||||
val renderReadReceipts: Boolean,
|
val renderReadReceipts: Boolean,
|
||||||
val highlightedEventId: EventId?,
|
val highlightedEventId: EventId?,
|
||||||
val backPaginationStatus: Timeline.PaginationStatus,
|
|
||||||
val newEventState: NewEventState,
|
val newEventState: NewEventState,
|
||||||
val eventSink: (TimelineEvents) -> Unit
|
val isLive: Boolean,
|
||||||
|
val focusedEventId : EventId?,
|
||||||
|
val focusRequestState: FocusRequestState,
|
||||||
|
val eventSink: (TimelineEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sealed interface FocusRequestState {
|
||||||
|
data object None : FocusRequestState
|
||||||
|
data class Cached(val index: Int): FocusRequestState
|
||||||
|
data object Fetching : FocusRequestState
|
||||||
|
data class Failure(val throwable: Throwable) : FocusRequestState
|
||||||
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class TimelineRoomInfo(
|
data class TimelineRoomInfo(
|
||||||
val isDm: Boolean,
|
val isDm: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -46,17 +46,18 @@ import kotlin.random.Random
|
||||||
|
|
||||||
fun aTimelineState(
|
fun aTimelineState(
|
||||||
timelineItems: ImmutableList<TimelineItem> = persistentListOf(),
|
timelineItems: ImmutableList<TimelineItem> = persistentListOf(),
|
||||||
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,
|
||||||
backPaginationStatus = paginationState,
|
|
||||||
renderReadReceipts = renderReadReceipts,
|
renderReadReceipts = renderReadReceipts,
|
||||||
highlightedEventId = null,
|
highlightedEventId = null,
|
||||||
newEventState = NewEventState.None,
|
newEventState = NewEventState.None,
|
||||||
|
isLive = true,
|
||||||
|
focusedEventId = null,
|
||||||
|
focusRequestState = FocusRequestState.None,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
package io.element.android.features.messages.impl.timeline
|
package io.element.android.features.messages.impl.timeline
|
||||||
|
|
||||||
import android.view.accessibility.AccessibilityManager
|
import android.view.accessibility.AccessibilityManager
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
|
@ -55,7 +56,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.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
|
||||||
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
import io.element.android.features.messages.impl.timeline.model.NewEventState
|
||||||
|
|
@ -63,8 +63,9 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider
|
||||||
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
import io.element.android.features.messages.impl.typing.TypingNotificationState
|
||||||
import io.element.android.features.messages.impl.typing.TypingNotificationView
|
|
||||||
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
import io.element.android.features.messages.impl.typing.aTypingNotificationState
|
||||||
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
|
||||||
|
|
@ -73,6 +74,8 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineView(
|
fun TimelineView(
|
||||||
|
|
@ -92,6 +95,10 @@ fun TimelineView(
|
||||||
forceJumpToBottomVisibility: Boolean = false
|
forceJumpToBottomVisibility: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
fun clearFocusRequestState() {
|
||||||
|
state.eventSink(TimelineEvents.ClearFocusRequestState)
|
||||||
|
}
|
||||||
|
|
||||||
fun onScrollFinishedAt(firstVisibleIndex: Int) {
|
fun onScrollFinishedAt(firstVisibleIndex: Int) {
|
||||||
state.eventSink(TimelineEvents.OnScrollFinished(firstVisibleIndex))
|
state.eventSink(TimelineEvents.OnScrollFinished(firstVisibleIndex))
|
||||||
}
|
}
|
||||||
|
|
@ -109,6 +116,10 @@ fun TimelineView(
|
||||||
// TODO implement this logic once we have support to 'jump to event X' in sliding sync
|
// TODO implement this logic once we have support to 'jump to event X' in sliding sync
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = state.timelineItems) {
|
||||||
|
Timber.d("TimelineView - timelineItem identifiers: ${state.timelineItems.joinToString(", ") { it.identifier() }}")
|
||||||
|
}
|
||||||
|
|
||||||
// Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms
|
// Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms
|
||||||
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
AnimatedVisibility(visible = true, enter = fadeIn()) {
|
||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
|
|
@ -118,9 +129,12 @@ fun TimelineView(
|
||||||
reverseLayout = useReverseLayout,
|
reverseLayout = useReverseLayout,
|
||||||
contentPadding = PaddingValues(vertical = 8.dp),
|
contentPadding = PaddingValues(vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
item {
|
/*
|
||||||
|
item(key = UUID.randomUUID()) {
|
||||||
TypingNotificationView(state = typingNotificationState)
|
TypingNotificationView(state = typingNotificationState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
items(
|
items(
|
||||||
items = state.timelineItems,
|
items = state.timelineItems,
|
||||||
contentType = { timelineItem -> timelineItem.contentType() },
|
contentType = { timelineItem -> timelineItem.contentType() },
|
||||||
|
|
@ -149,30 +163,68 @@ fun TimelineView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FocusRequestStateView(
|
||||||
|
focusRequestState = state.focusRequestState,
|
||||||
|
onClearFocusRequestState = ::clearFocusRequestState
|
||||||
|
)
|
||||||
|
|
||||||
TimelineScrollHelper(
|
TimelineScrollHelper(
|
||||||
isTimelineEmpty = state.timelineItems.isEmpty(),
|
isTimelineEmpty = state.timelineItems.isEmpty(),
|
||||||
lazyListState = lazyListState,
|
lazyListState = lazyListState,
|
||||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||||
newEventState = state.newEventState,
|
newEventState = state.newEventState,
|
||||||
onScrollFinishedAt = ::onScrollFinishedAt
|
isLive = state.isLive,
|
||||||
|
focusRequestState = state.focusRequestState,
|
||||||
|
onScrollFinishedAt = ::onScrollFinishedAt,
|
||||||
|
onClearFocusRequestState = ::clearFocusRequestState,
|
||||||
|
onJumpToLive = { state.eventSink(TimelineEvents.JumpToLive) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FocusRequestStateView(
|
||||||
|
focusRequestState: FocusRequestState,
|
||||||
|
onClearFocusRequestState: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
BackHandler(enabled = focusRequestState is FocusRequestState.Fetching) {
|
||||||
|
onClearFocusRequestState()
|
||||||
|
}
|
||||||
|
|
||||||
|
when (focusRequestState) {
|
||||||
|
is FocusRequestState.Failure -> {
|
||||||
|
ErrorDialog(
|
||||||
|
content = stringResource(id = CommonStrings.common_failed),
|
||||||
|
onDismiss = onClearFocusRequestState,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
FocusRequestState.Fetching -> {
|
||||||
|
ProgressDialog(modifier = modifier)
|
||||||
|
}
|
||||||
|
is FocusRequestState.Cached, FocusRequestState.None -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BoxScope.TimelineScrollHelper(
|
private fun BoxScope.TimelineScrollHelper(
|
||||||
isTimelineEmpty: Boolean,
|
isTimelineEmpty: Boolean,
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
newEventState: NewEventState,
|
newEventState: NewEventState,
|
||||||
|
isLive: Boolean,
|
||||||
forceJumpToBottomVisibility: Boolean,
|
forceJumpToBottomVisibility: Boolean,
|
||||||
|
focusRequestState: FocusRequestState,
|
||||||
|
onClearFocusRequestState: () -> Unit,
|
||||||
onScrollFinishedAt: (Int) -> Unit,
|
onScrollFinishedAt: (Int) -> Unit,
|
||||||
|
onJumpToLive: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val isScrollFinished by remember { derivedStateOf { !lazyListState.isScrollInProgress } }
|
val isScrollFinished by remember { derivedStateOf { !lazyListState.isScrollInProgress } }
|
||||||
val canAutoScroll by remember {
|
val canAutoScroll by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
lazyListState.firstVisibleItemIndex < 3
|
lazyListState.firstVisibleItemIndex < 3 && isLive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,9 +238,29 @@ private fun BoxScope.TimelineScrollHelper(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun jumpToBottom() {
|
||||||
|
if (isLive) {
|
||||||
|
scrollToBottom()
|
||||||
|
} else {
|
||||||
|
onJumpToLive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = focusRequestState) {
|
||||||
|
if (focusRequestState is FocusRequestState.Cached) {
|
||||||
|
if (abs(lazyListState.firstVisibleItemIndex - focusRequestState.index) < 10) {
|
||||||
|
lazyListState.animateScrollToItem(focusRequestState.index)
|
||||||
|
} else {
|
||||||
|
lazyListState.scrollToItem(focusRequestState.index)
|
||||||
|
}
|
||||||
|
onClearFocusRequestState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(canAutoScroll, newEventState) {
|
LaunchedEffect(canAutoScroll, newEventState) {
|
||||||
val shouldAutoScroll = isScrollFinished && (canAutoScroll || newEventState == NewEventState.FromMe)
|
Timber.d("TimelineScrollHelper - canAutoScroll: $canAutoScroll, newEventState: $newEventState")
|
||||||
if (shouldAutoScroll) {
|
val shouldScrollToBottom = isScrollFinished && (canAutoScroll || newEventState == NewEventState.FromMe)
|
||||||
|
if (shouldScrollToBottom) {
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -203,11 +275,11 @@ private fun BoxScope.TimelineScrollHelper(
|
||||||
|
|
||||||
JumpToBottomButton(
|
JumpToBottomButton(
|
||||||
// Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered
|
// Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered
|
||||||
isVisible = !canAutoScroll || forceJumpToBottomVisibility,
|
isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(end = 24.dp, bottom = 12.dp),
|
.padding(end = 24.dp, bottom = 12.dp),
|
||||||
onClick = ::scrollToBottom,
|
onClick = { jumpToBottom() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.components
|
package io.element.android.features.messages.impl.timeline.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -29,6 +30,7 @@ import io.element.android.features.messages.impl.timeline.components.virtual.Tim
|
||||||
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.TimelineItemInvisibleIndicatorModel
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel
|
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.TimelineItemRoomBeginningModel
|
||||||
|
|
@ -46,10 +48,11 @@ fun TimelineItemVirtualRow(
|
||||||
is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(modifier)
|
is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(modifier)
|
||||||
TimelineItemRoomBeginningModel -> TimelineItemRoomBeginningView(roomName = timelineRoomInfo.name, modifier = modifier)
|
TimelineItemRoomBeginningModel -> TimelineItemRoomBeginningView(roomName = timelineRoomInfo.name, modifier = modifier)
|
||||||
is TimelineItemLoadingIndicatorModel -> {
|
is TimelineItemLoadingIndicatorModel -> {
|
||||||
TimelineLoadingMoreIndicator()
|
TimelineLoadingMoreIndicator(modifier)
|
||||||
LaunchedEffect(key1 = virtual.model.timestamp) {
|
LaunchedEffect(key1 = virtual.model.timestamp) {
|
||||||
eventSink(TimelineEvents.LoadMore(virtual.model.backwards))
|
eventSink(TimelineEvents.LoadMore(virtual.model.direction))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TimelineItemInvisibleIndicatorModel -> Spacer(modifier = modifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.components.virtual
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -26,19 +27,19 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.LinearProgressIndicator
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TimelineLoadingMoreIndicator(modifier: Modifier = Modifier) {
|
internal fun TimelineLoadingMoreIndicator(modifier: Modifier = Modifier) {
|
||||||
Box(
|
Box(
|
||||||
modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.padding(2.dp),
|
||||||
.padding(8.dp),
|
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator(
|
LinearProgressIndicator(modifier = Modifier
|
||||||
strokeWidth = 2.dp,
|
.height(1.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.timeline.factories
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
|
||||||
import io.element.android.features.messages.impl.timeline.diff.TimelineItemsCacheInvalidator
|
import io.element.android.features.messages.impl.timeline.diff.TimelineItemsCacheInvalidator
|
||||||
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemEventFactory
|
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemEventFactory
|
||||||
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
|
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
|
||||||
|
|
@ -33,6 +34,7 @@ import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
@ -43,9 +45,9 @@ class TimelineItemsFactory @Inject constructor(
|
||||||
private val eventItemFactory: TimelineItemEventFactory,
|
private val eventItemFactory: TimelineItemEventFactory,
|
||||||
private val virtualItemFactory: TimelineItemVirtualFactory,
|
private val virtualItemFactory: TimelineItemVirtualFactory,
|
||||||
private val timelineItemGrouper: TimelineItemGrouper,
|
private val timelineItemGrouper: TimelineItemGrouper,
|
||||||
|
private val timelineItemIndexer: TimelineItemIndexer,
|
||||||
) {
|
) {
|
||||||
private val timelineItems = MutableStateFlow(persistentListOf<TimelineItem>())
|
private val timelineItems = MutableStateFlow(persistentListOf<TimelineItem>())
|
||||||
|
|
||||||
private val lock = Mutex()
|
private val lock = Mutex()
|
||||||
private val diffCache = MutableListDiffCache<TimelineItem>()
|
private val diffCache = MutableListDiffCache<TimelineItem>()
|
||||||
private val diffCacheUpdater = DiffCacheUpdater<MatrixTimelineItem, TimelineItem>(
|
private val diffCacheUpdater = DiffCacheUpdater<MatrixTimelineItem, TimelineItem>(
|
||||||
|
|
@ -60,6 +62,10 @@ class TimelineItemsFactory @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun items(): StateFlow<ImmutableList<TimelineItem>> {
|
||||||
|
return timelineItems
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun collectItemsAsState(): State<ImmutableList<TimelineItem>> {
|
fun collectItemsAsState(): State<ImmutableList<TimelineItem>> {
|
||||||
return timelineItems.collectAsState()
|
return timelineItems.collectAsState()
|
||||||
|
|
@ -80,6 +86,7 @@ class TimelineItemsFactory @Inject constructor(
|
||||||
roomMembers: List<RoomMember>,
|
roomMembers: List<RoomMember>,
|
||||||
) {
|
) {
|
||||||
val newTimelineItemStates = ArrayList<TimelineItem>()
|
val newTimelineItemStates = ArrayList<TimelineItem>()
|
||||||
|
val newTimelineById = mutableMapOf<String, TimelineItem>()
|
||||||
for (index in diffCache.indices().reversed()) {
|
for (index in diffCache.indices().reversed()) {
|
||||||
val cacheItem = diffCache.get(index)
|
val cacheItem = diffCache.get(index)
|
||||||
if (cacheItem == null) {
|
if (cacheItem == null) {
|
||||||
|
|
@ -96,10 +103,12 @@ class TimelineItemsFactory @Inject constructor(
|
||||||
} else {
|
} else {
|
||||||
cacheItem
|
cacheItem
|
||||||
}
|
}
|
||||||
|
newTimelineById[updatedItem.identifier()] = updatedItem
|
||||||
newTimelineItemStates.add(updatedItem)
|
newTimelineItemStates.add(updatedItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val result = timelineItemGrouper.group(newTimelineItemStates).toPersistentList()
|
val result = timelineItemGrouper.group(newTimelineItemStates).toPersistentList()
|
||||||
|
timelineItemIndexer.process(result)
|
||||||
this.timelineItems.emit(result)
|
this.timelineItems.emit(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,13 +117,13 @@ class TimelineItemsFactory @Inject constructor(
|
||||||
index: Int,
|
index: Int,
|
||||||
roomMembers: List<RoomMember>,
|
roomMembers: List<RoomMember>,
|
||||||
): TimelineItem? {
|
): TimelineItem? {
|
||||||
val timelineItemState =
|
val timelineItem =
|
||||||
when (val currentTimelineItem = timelineItems[index]) {
|
when (val currentTimelineItem = timelineItems[index]) {
|
||||||
is MatrixTimelineItem.Event -> eventItemFactory.create(currentTimelineItem, index, timelineItems, roomMembers)
|
is MatrixTimelineItem.Event -> eventItemFactory.create(currentTimelineItem, index, timelineItems, roomMembers)
|
||||||
is MatrixTimelineItem.Virtual -> virtualItemFactory.create(currentTimelineItem)
|
is MatrixTimelineItem.Virtual -> virtualItemFactory.create(currentTimelineItem)
|
||||||
MatrixTimelineItem.Other -> null
|
MatrixTimelineItem.Other -> null
|
||||||
}
|
}
|
||||||
diffCache[index] = timelineItemState
|
diffCache[index] = timelineItem
|
||||||
return timelineItemState
|
return timelineItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ 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.TimelineItemInvisibleIndicatorModel
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel
|
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.TimelineItemRoomBeginningModel
|
||||||
|
|
@ -45,9 +46,10 @@ class TimelineItemVirtualFactory @Inject constructor(
|
||||||
is VirtualTimelineItem.EncryptedHistoryBanner -> TimelineItemEncryptedHistoryBannerVirtualModel
|
is VirtualTimelineItem.EncryptedHistoryBanner -> TimelineItemEncryptedHistoryBannerVirtualModel
|
||||||
is VirtualTimelineItem.RoomBeginning -> TimelineItemRoomBeginningModel
|
is VirtualTimelineItem.RoomBeginning -> TimelineItemRoomBeginningModel
|
||||||
is VirtualTimelineItem.LoadingIndicator -> TimelineItemLoadingIndicatorModel(
|
is VirtualTimelineItem.LoadingIndicator -> TimelineItemLoadingIndicatorModel(
|
||||||
backwards = inner.backwards,
|
direction = inner.direction,
|
||||||
timestamp = inner.timestamp
|
timestamp = inner.timestamp
|
||||||
)
|
)
|
||||||
|
VirtualTimelineItem.LatestKnownEventIndicator -> TimelineItemInvisibleIndicatorModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.timeline
|
package io.element.android.features.messages.impl.timeline.model.virtual
|
||||||
|
|
||||||
class RustDetachedTimeline {
|
data object TimelineItemInvisibleIndicatorModel : TimelineItemVirtualModel {
|
||||||
|
override val type: String = "TimelineItemInvisibleIndicatorModel"
|
||||||
}
|
}
|
||||||
|
|
@ -16,8 +16,10 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.timeline.model.virtual
|
package io.element.android.features.messages.impl.timeline.model.virtual
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
|
||||||
data class TimelineItemLoadingIndicatorModel(
|
data class TimelineItemLoadingIndicatorModel(
|
||||||
val backwards: Boolean,
|
val direction: Timeline.PaginationDirection,
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
) : TimelineItemVirtualModel {
|
) : TimelineItemVirtualModel {
|
||||||
override val type: String = "TimelineItemLoadingIndicatorModel"
|
override val type: String = "TimelineItemLoadingIndicatorModel"
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ class PollHistoryPresenter @Inject constructor(
|
||||||
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.liveTimeline
|
val timeline = room.liveTimeline
|
||||||
val paginationState by timeline.backPaginationStatus.collectAsState()
|
val paginationState by timeline.paginationStatus(Timeline.PaginationDirection.BACKWARDS).collectAsState()
|
||||||
val pollHistoryItemsFlow = remember {
|
val pollHistoryItemsFlow = remember {
|
||||||
timeline.timelineItems.map { items ->
|
timeline.timelineItems.map { items ->
|
||||||
pollHistoryItemFactory.create(items)
|
pollHistoryItemFactory.create(items)
|
||||||
|
|
@ -96,6 +96,6 @@ class PollHistoryPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.loadMore(pollHistory: Timeline) = launch {
|
private fun CoroutineScope.loadMore(pollHistory: Timeline) = launch {
|
||||||
pollHistory.paginateBackwards()
|
pollHistory.paginate(Timeline.PaginationDirection.BACKWARDS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,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.LiveTimeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
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
|
||||||
|
|
@ -98,7 +98,9 @@ interface MatrixRoom : Closeable {
|
||||||
|
|
||||||
val syncUpdateFlow: StateFlow<Long>
|
val syncUpdateFlow: StateFlow<Long>
|
||||||
|
|
||||||
val liveTimeline: LiveTimeline
|
val liveTimeline: Timeline
|
||||||
|
|
||||||
|
suspend fun timelineFocusedOnEvent(eventId: EventId): Timeline
|
||||||
|
|
||||||
fun destroy()
|
fun destroy()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
/*
|
|
||||||
* 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>
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
/*
|
|
||||||
* 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>
|
|
||||||
}
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.timeline
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
|
@ -28,7 +29,14 @@ interface Timeline : AutoCloseable {
|
||||||
val canPaginate: Boolean = !isPaginating && hasMoreToLoad
|
val canPaginate: Boolean = !isPaginating && hasMoreToLoad
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun paginateBackwards(): Result<Boolean>
|
enum class PaginationDirection {
|
||||||
val backPaginationStatus: StateFlow<PaginationStatus>
|
BACKWARDS,
|
||||||
|
FORWARDS
|
||||||
|
}
|
||||||
|
|
||||||
|
val membershipChangeEventReceived: Flow<Unit>
|
||||||
|
suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result<Unit>
|
||||||
|
suspend fun paginate(direction: PaginationDirection): Result<Boolean>
|
||||||
|
fun paginationStatus(direction: PaginationDirection): StateFlow<PaginationStatus>
|
||||||
val timelineItems: Flow<List<MatrixTimelineItem>>
|
val timelineItems: Flow<List<MatrixTimelineItem>>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.timeline.item.virtual
|
package io.element.android.libraries.matrix.api.timeline.item.virtual
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
|
||||||
sealed interface VirtualTimelineItem {
|
sealed interface VirtualTimelineItem {
|
||||||
data class DayDivider(
|
data class DayDivider(
|
||||||
val timestamp: Long
|
val timestamp: Long
|
||||||
|
|
@ -27,8 +29,10 @@ sealed interface VirtualTimelineItem {
|
||||||
|
|
||||||
data object RoomBeginning: VirtualTimelineItem
|
data object RoomBeginning: VirtualTimelineItem
|
||||||
|
|
||||||
|
data object LatestKnownEventIndicator: VirtualTimelineItem
|
||||||
|
|
||||||
data class LoadingIndicator(
|
data class LoadingIndicator(
|
||||||
val backwards: Boolean,
|
val direction: Timeline.PaginationDirection,
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
): VirtualTimelineItem
|
): VirtualTimelineItem
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,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.LiveTimeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
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
|
||||||
|
|
@ -57,7 +57,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.RustLiveTimeline
|
import io.element.android.libraries.matrix.impl.timeline.RustTimeline
|
||||||
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
|
||||||
|
|
@ -160,7 +160,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 liveTimeline = createLiveTimeline(innerTimeline){
|
override val liveTimeline = createTimeline(innerTimeline, isLive = true){
|
||||||
_syncUpdateFlow.value = systemClock.epochMillis()
|
_syncUpdateFlow.value = systemClock.epochMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,6 +183,12 @@ class RustMatrixRoom(
|
||||||
|
|
||||||
override suspend fun unsubscribeFromSync() = roomSyncSubscriber.unsubscribe(roomId)
|
override suspend fun unsubscribeFromSync() = roomSyncSubscriber.unsubscribe(roomId)
|
||||||
|
|
||||||
|
override suspend fun timelineFocusedOnEvent(eventId: EventId): Timeline {
|
||||||
|
return innerRoom.timelineFocusedOnEvent(eventId.value, numContextEvents = 50u).let {inner ->
|
||||||
|
createTimeline(inner, isLive = false){}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun destroy() {
|
override fun destroy() {
|
||||||
roomCoroutineScope.cancel()
|
roomCoroutineScope.cancel()
|
||||||
liveTimeline.close()
|
liveTimeline.close()
|
||||||
|
|
@ -745,12 +751,14 @@ class RustMatrixRoom(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createLiveTimeline(
|
private fun createTimeline(
|
||||||
timeline: InnerTimeline,
|
timeline: InnerTimeline,
|
||||||
|
isLive: Boolean = true,
|
||||||
onNewSyncedEvent: () -> Unit = {},
|
onNewSyncedEvent: () -> Unit = {},
|
||||||
): LiveTimeline {
|
): Timeline {
|
||||||
return RustLiveTimeline(
|
return RustTimeline(
|
||||||
isKeyBackupEnabled = isKeyBackupEnabled,
|
isKeyBackupEnabled = isKeyBackupEnabled,
|
||||||
|
isLive = isLive,
|
||||||
matrixRoom = this,
|
matrixRoom = this,
|
||||||
systemClock = systemClock,
|
systemClock = systemClock,
|
||||||
roomCoroutineScope = roomCoroutineScope,
|
roomCoroutineScope = roomCoroutineScope,
|
||||||
|
|
|
||||||
|
|
@ -1,55 +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.room.MatrixRoom
|
|
||||||
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.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.RoomBeginningPostProcessor
|
|
||||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor
|
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.ensureActive
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.matrix.rustcomponents.sdk.PaginationOptions
|
|
||||||
import org.matrix.rustcomponents.sdk.Timeline
|
|
||||||
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
|
|
||||||
|
|
||||||
private const val INITIAL_MAX_SIZE = 50
|
|
||||||
|
|
||||||
|
|
@ -18,7 +18,6 @@ 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.LiveTimeline
|
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||||
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
import io.element.android.libraries.matrix.api.timeline.ReceiptType
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
|
|
@ -27,6 +26,7 @@ import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessage
|
||||||
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.InvisibleIndicatorPostProcessor
|
||||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.LoadingIndicatorsPostProcessor
|
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.RoomBeginningPostProcessor
|
||||||
import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor
|
import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor
|
||||||
|
|
@ -42,6 +42,7 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
@ -58,8 +59,9 @@ import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
|
||||||
|
|
||||||
private const val INITIAL_MAX_SIZE = 50
|
private const val INITIAL_MAX_SIZE = 50
|
||||||
|
|
||||||
class RustLiveTimeline(
|
class RustTimeline(
|
||||||
private val inner: InnerTimeline,
|
private val inner: InnerTimeline,
|
||||||
|
private val isLive: Boolean,
|
||||||
private val systemClock: SystemClock,
|
private val systemClock: SystemClock,
|
||||||
private val roomCoroutineScope: CoroutineScope,
|
private val roomCoroutineScope: CoroutineScope,
|
||||||
private val isKeyBackupEnabled: Boolean,
|
private val isKeyBackupEnabled: Boolean,
|
||||||
|
|
@ -68,7 +70,7 @@ class RustLiveTimeline(
|
||||||
private val lastLoginTimestamp: Date?,
|
private val lastLoginTimestamp: Date?,
|
||||||
private val fetchDetailsForEvent: suspend (EventId) -> Result<Unit>,
|
private val fetchDetailsForEvent: suspend (EventId) -> Result<Unit>,
|
||||||
private val onNewSyncedEvent: () -> Unit,
|
private val onNewSyncedEvent: () -> Unit,
|
||||||
) : LiveTimeline {
|
) : Timeline {
|
||||||
|
|
||||||
private val initLatch = CompletableDeferred<Unit>()
|
private val initLatch = CompletableDeferred<Unit>()
|
||||||
private val isInit = AtomicBoolean(false)
|
private val isInit = AtomicBoolean(false)
|
||||||
|
|
@ -85,6 +87,7 @@ class RustLiveTimeline(
|
||||||
|
|
||||||
private val roomBeginningPostProcessor = RoomBeginningPostProcessor()
|
private val roomBeginningPostProcessor = RoomBeginningPostProcessor()
|
||||||
private val loadingIndicatorsPostProcessor = LoadingIndicatorsPostProcessor(systemClock)
|
private val loadingIndicatorsPostProcessor = LoadingIndicatorsPostProcessor(systemClock)
|
||||||
|
private val invisibleIndicatorPostProcessor = InvisibleIndicatorPostProcessor(isLive)
|
||||||
|
|
||||||
private val timelineItemFactory = MatrixTimelineItemMapper(
|
private val timelineItemFactory = MatrixTimelineItemMapper(
|
||||||
fetchDetailsForEvent = fetchDetailsForEvent,
|
fetchDetailsForEvent = fetchDetailsForEvent,
|
||||||
|
|
@ -127,35 +130,59 @@ class RustLiveTimeline(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun paginateBackwards(): Result<Boolean> {
|
override suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> {
|
||||||
initLatch.await()
|
initLatch.await()
|
||||||
return runCatching {
|
return runCatching {
|
||||||
if (!canBackPaginate()) throw TimelineException.CannotPaginate
|
if (!canPaginate(direction)) throw TimelineException.CannotPaginate
|
||||||
inner.paginateBackwards()
|
when (direction) {
|
||||||
|
Timeline.PaginationDirection.BACKWARDS -> inner.paginateBackwards(50u)
|
||||||
|
Timeline.PaginationDirection.FORWARDS -> inner.paginateForwards(50u)
|
||||||
|
}
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
if (error is TimelineException.CannotPaginate) {
|
if (error is TimelineException.CannotPaginate) {
|
||||||
Timber.d("Can't paginate backwards on room ${matrixRoom.roomId} with backPaginationStatus: ${backPaginationStatus.value}")
|
Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backPaginationStatus.value}")
|
||||||
} else {
|
} else {
|
||||||
Timber.e(error, "Error paginating backwards on room ${matrixRoom.roomId}")
|
Timber.e(error, "Error paginating $direction on room ${matrixRoom.roomId}")
|
||||||
}
|
}
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
Timber.v("Success back paginating for room ${matrixRoom.roomId}")
|
Timber.v("Success paginating $direction for room ${matrixRoom.roomId}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun canBackPaginate(): Boolean {
|
private fun canPaginate(direction: Timeline.PaginationDirection): Boolean {
|
||||||
return isInit.get() && backPaginationStatus.value.canPaginate
|
if (!isInit.get()) return false
|
||||||
|
return when (direction) {
|
||||||
|
Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus.value.canPaginate
|
||||||
|
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.value.canPaginate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val backPaginationStatus: StateFlow<Timeline.PaginationStatus> = inner
|
override fun paginationStatus(direction: Timeline.PaginationDirection): StateFlow<Timeline.PaginationStatus> {
|
||||||
|
return when (direction) {
|
||||||
|
Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus
|
||||||
|
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val backPaginationStatus: StateFlow<Timeline.PaginationStatus> = inner
|
||||||
.backPaginationStatusFlow()
|
.backPaginationStatusFlow()
|
||||||
.map()
|
.map()
|
||||||
.stateIn(roomCoroutineScope, SharingStarted.Eagerly, Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true))
|
.stateIn(roomCoroutineScope, SharingStarted.Eagerly, Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true))
|
||||||
|
|
||||||
|
private val forwardPaginationStatus: StateFlow<Timeline.PaginationStatus> =
|
||||||
|
when (isLive) {
|
||||||
|
true -> MutableStateFlow(Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = false))
|
||||||
|
false -> inner
|
||||||
|
.forwardPaginationStatusFlow()
|
||||||
|
.map()
|
||||||
|
.stateIn(roomCoroutineScope, SharingStarted.Eagerly, Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true))
|
||||||
|
}
|
||||||
|
|
||||||
override val timelineItems: Flow<List<MatrixTimelineItem>> = combine(
|
override val timelineItems: Flow<List<MatrixTimelineItem>> = combine(
|
||||||
_timelineItems,
|
_timelineItems,
|
||||||
backPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged()
|
backPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged(),
|
||||||
) { timelineItems, hasMoreToLoadBackward ->
|
forwardPaginationStatus.map { it.hasMoreToLoad }.distinctUntilChanged(),
|
||||||
|
) { timelineItems, hasMoreToLoadBackward, hasMoreToLoadForward ->
|
||||||
timelineItems
|
timelineItems
|
||||||
.let { items -> encryptedHistoryPostProcessor.process(items) }
|
.let { items -> encryptedHistoryPostProcessor.process(items) }
|
||||||
.let { items ->
|
.let { items ->
|
||||||
|
|
@ -164,7 +191,8 @@ class RustLiveTimeline(
|
||||||
isDm = matrixRoom.isDm,
|
isDm = matrixRoom.isDm,
|
||||||
hasMoreToLoadBackwards = hasMoreToLoadBackward
|
hasMoreToLoadBackwards = hasMoreToLoadBackward
|
||||||
)
|
)
|
||||||
}.let {items -> loadingIndicatorsPostProcessor.process(items, hasMoreToLoadBackward)}
|
}.let { items -> loadingIndicatorsPostProcessor.process(items, hasMoreToLoadBackward, hasMoreToLoadForward) }
|
||||||
|
.let { items -> invisibleIndicatorPostProcessor.process(items) }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
class InvisibleIndicatorPostProcessor(
|
||||||
|
private val isLive: Boolean,
|
||||||
|
) {
|
||||||
|
private val latestEventIdentifiers: MutableSet<String> = HashSet()
|
||||||
|
|
||||||
|
fun process(
|
||||||
|
items: List<MatrixTimelineItem>,
|
||||||
|
): List<MatrixTimelineItem> {
|
||||||
|
if (isLive) {
|
||||||
|
return items
|
||||||
|
} else {
|
||||||
|
return buildList {
|
||||||
|
items.forEach { item ->
|
||||||
|
add(item)
|
||||||
|
if (item is MatrixTimelineItem.Event) {
|
||||||
|
if (latestEventIdentifiers.contains(item.uniqueId)) {
|
||||||
|
add(createLatestKnownEventIndicator(item.uniqueId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items.latestEventIdentifier()?.let { latestEventIdentifier ->
|
||||||
|
if (latestEventIdentifiers.add(latestEventIdentifier)) {
|
||||||
|
add(createLatestKnownEventIndicator(latestEventIdentifier))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createLatestKnownEventIndicator(identifier: String): MatrixTimelineItem {
|
||||||
|
return MatrixTimelineItem.Virtual(
|
||||||
|
uniqueId = "latest_known_event_$identifier",
|
||||||
|
virtual = VirtualTimelineItem.LatestKnownEventIndicator
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<MatrixTimelineItem>.latestEventIdentifier(): String? {
|
||||||
|
return findLast {
|
||||||
|
when (it) {
|
||||||
|
is MatrixTimelineItem.Event -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}?.let {
|
||||||
|
(it as MatrixTimelineItem.Event).uniqueId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<MatrixTimelineItem>.indexOf(identifier: String): Int {
|
||||||
|
return indexOfLast {
|
||||||
|
when (it) {
|
||||||
|
is MatrixTimelineItem.Event -> {
|
||||||
|
it.uniqueId == identifier
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.libraries.matrix.impl.timeline.postprocessor
|
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.MatrixTimelineItem
|
||||||
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||||
|
|
||||||
|
|
@ -24,21 +25,34 @@ class LoadingIndicatorsPostProcessor(private val systemClock: SystemClock) {
|
||||||
|
|
||||||
fun process(
|
fun process(
|
||||||
items: List<MatrixTimelineItem>,
|
items: List<MatrixTimelineItem>,
|
||||||
hasMoreToLoadBackwards: Boolean,
|
hasMoreToLoadBackward: Boolean,
|
||||||
|
hasMoreToLoadForward: Boolean,
|
||||||
): List<MatrixTimelineItem> {
|
): List<MatrixTimelineItem> {
|
||||||
return if (hasMoreToLoadBackwards && !items.hasEncryptionHistoryBanner()){
|
val shouldAddBackwardLoadingIndicator = hasMoreToLoadBackward && !items.hasEncryptionHistoryBanner()
|
||||||
listOf(
|
val shouldAddForwardLoadingIndicator = hasMoreToLoadForward && items.isNotEmpty()
|
||||||
MatrixTimelineItem.Virtual(
|
val currentTimestamp = systemClock.epochMillis()
|
||||||
|
return buildList {
|
||||||
|
if (shouldAddBackwardLoadingIndicator) {
|
||||||
|
val backwardLoadingIndicator = MatrixTimelineItem.Virtual(
|
||||||
uniqueId = "BackwardLoadingIndicator",
|
uniqueId = "BackwardLoadingIndicator",
|
||||||
virtual = VirtualTimelineItem.LoadingIndicator(
|
virtual = VirtualTimelineItem.LoadingIndicator(
|
||||||
backwards = true,
|
direction = Timeline.PaginationDirection.BACKWARDS,
|
||||||
timestamp = systemClock.epochMillis()
|
timestamp = currentTimestamp
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) + items
|
add(backwardLoadingIndicator)
|
||||||
}else {
|
}
|
||||||
items
|
addAll(items)
|
||||||
|
if (shouldAddForwardLoadingIndicator) {
|
||||||
|
val forwardLoadingIndicator = MatrixTimelineItem.Virtual(
|
||||||
|
uniqueId = "ForwardLoadingIndicator",
|
||||||
|
virtual = VirtualTimelineItem.LoadingIndicator(
|
||||||
|
direction = Timeline.PaginationDirection.FORWARDS,
|
||||||
|
timestamp = currentTimestamp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
add(forwardLoadingIndicator)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue