ganfra
5 months ago
25 changed files with 599 additions and 218 deletions
@ -0,0 +1,135 @@
@@ -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 |
||||
} |
||||
} |
@ -0,0 +1,65 @@
@@ -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 |
||||
} |
||||
} |
@ -1,24 +0,0 @@
@@ -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 @@
@@ -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> |
||||
} |
@ -1,55 +0,0 @@
@@ -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 |
||||
|
@ -0,0 +1,79 @@
@@ -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 |
||||
} |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue