From 9238037067c9131391c79ad8cd99e8b52ee75831 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 7 Sep 2021 19:28:20 +0200 Subject: [PATCH 01/29] Timeline: try new strategy for handling chunks (no merging) --- .../sdk/api/session/room/timeline/Timeline.kt | 15 + .../sdk/internal/database/DatabaseCleaner.kt | 20 +- .../SessionRealmConfigurationFactory.kt | 1 + .../database/helper/ChunkEntityHelper.kt | 2 +- .../internal/database/model/ChunkEntity.kt | 4 +- .../session/room/timeline/DefaultTimeline.kt | 873 +++++------------- .../room/timeline/DefaultTimelineService.kt | 7 +- .../{TimelineState.kt => LoadMoreResult.kt} | 13 +- .../room/timeline/LoadTimelineStrategy.kt | 198 ++++ .../room/timeline/SendingEventsDataSource.kt | 84 ++ .../session/room/timeline/TimelineChunk.kt | 362 ++++++++ .../room/timeline/TokenChunkEventPersistor.kt | 79 +- .../session/room/timeline/UIEchoManager.kt | 10 +- .../timeline/TimelineEventController.kt | 15 +- 14 files changed, 929 insertions(+), 754 deletions(-) rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/{TimelineState.kt => LoadMoreResult.kt} (70%) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt index 06c88db831..dff45c0d94 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt @@ -95,6 +95,8 @@ interface Timeline { */ fun getTimelineEventWithId(eventId: String?): TimelineEvent? + fun getPaginationState(direction: Direction): PaginationState + interface Listener { /** * Call when the timeline has been updated through pagination or sync. @@ -112,8 +114,21 @@ interface Timeline { * Called when new events come through the sync */ fun onNewTimelineEvents(eventIds: List) + + fun onStateUpdated() { + //NOOP + } } + /** + * Pagination state + */ + data class PaginationState( + val hasMoreToLoad: Boolean = true, + val loading: Boolean = false, + val inError: Boolean = false + ) + /** * This is used to paginate in one or another direction. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt index ee58880eb8..09fbb2bfa0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt @@ -16,22 +16,13 @@ package org.matrix.android.sdk.internal.database -import io.realm.Realm import io.realm.RealmConfiguration import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.internal.database.helper.nextDisplayIndex -import org.matrix.android.sdk.internal.database.model.ChunkEntity -import org.matrix.android.sdk.internal.database.model.ChunkEntityFields -import org.matrix.android.sdk.internal.database.model.EventEntity -import org.matrix.android.sdk.internal.database.model.RoomEntity -import org.matrix.android.sdk.internal.database.model.TimelineEventEntity -import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields -import org.matrix.android.sdk.internal.database.model.deleteOnCascade -import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.api.session.SessionLifecycleObserver -import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.task.TaskExecutor import timber.log.Timber import javax.inject.Inject @@ -53,11 +44,12 @@ internal class DatabaseCleaner @Inject constructor(@SessionDatabase private val awaitTransaction(realmConfiguration) { realm -> val allRooms = realm.where(RoomEntity::class.java).findAll() Timber.v("There are ${allRooms.size} rooms in this session") - cleanUp(realm, MAX_NUMBER_OF_EVENTS_IN_DB / 2L) + //cleanUp(realm, MAX_NUMBER_OF_EVENTS_IN_DB / 2L) } } } + /* private fun cleanUp(realm: Realm, threshold: Long) { val numberOfEvents = realm.where(EventEntity::class.java).findAll().size val numberOfTimelineEvents = realm.where(TimelineEventEntity::class.java).findAll().size @@ -75,7 +67,7 @@ internal class DatabaseCleaner @Inject constructor(@SessionDatabase private val val thresholdDisplayIndex = maxDisplayIndex - threshold val eventsToRemove = chunk.timelineEvents.where().lessThan(TimelineEventEntityFields.DISPLAY_INDEX, thresholdDisplayIndex).findAll() Timber.v("There are ${eventsToRemove.size} events to clean in chunk: ${chunk.identifier()} from room ${chunk.room?.first()?.roomId}") - chunk.numberOfTimelineEvents = chunk.numberOfTimelineEvents - eventsToRemove.size + //chunk.numberOfTimelineEvents = chunk.numberOfTimelineEvents - eventsToRemove.size eventsToRemove.forEach { val canDeleteRoot = it.root?.stateKey == null it.deleteOnCascade(canDeleteRoot) @@ -86,4 +78,6 @@ internal class DatabaseCleaner @Inject constructor(@SessionDatabase private val cleanUp(realm, (threshold / 1.5).toLong()) } } + + */ } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt index 1771c5b202..6aeb3936a9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt @@ -72,6 +72,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor( .modules(SessionRealmModule()) .schemaVersion(RealmSessionStoreMigration.SESSION_STORE_SCHEMA_VERSION) .migration(RealmSessionStoreMigration) + .deleteRealmIfMigrationNeeded() .build() // Try creating a realm instance and if it succeeds we can clear the flag diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt index e262b40419..5e4e5ff3b6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt @@ -110,7 +110,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String, true } } - numberOfTimelineEvents++ + //numberOfTimelineEvents++ timelineEvents.add(timelineEventEntity) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt index 68533a3c19..8b301d15fa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt @@ -27,9 +27,11 @@ import org.matrix.android.sdk.internal.extensions.clearWith internal open class ChunkEntity(@Index var prevToken: String? = null, // Because of gaps we can have several chunks with nextToken == null @Index var nextToken: String? = null, + var prevChunk: ChunkEntity? = null, + var nextChunk: ChunkEntity? = null, var stateEvents: RealmList = RealmList(), var timelineEvents: RealmList = RealmList(), - var numberOfTimelineEvents: Long = 0, + //var numberOfTimelineEvents: Long = 0, // Only one chunk will have isLastForward == true @Index var isLastForward: Boolean = false, @Index var isLastBackward: Boolean = false diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 8cc5d943b7..141d33ad0e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright (c) 2021 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. @@ -16,168 +16,257 @@ package org.matrix.android.sdk.internal.session.room.timeline -import io.realm.OrderedCollectionChangeSet -import io.realm.OrderedRealmCollectionChangeListener import io.realm.Realm import io.realm.RealmConfiguration -import io.realm.RealmQuery -import io.realm.RealmResults -import io.realm.Sort -import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.extensions.orFalse +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.android.asCoroutineDispatcher +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.internal.closeQuietly import org.matrix.android.sdk.api.extensions.tryOrNull -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.api.util.CancelableBag -import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper -import org.matrix.android.sdk.internal.database.model.ChunkEntity -import org.matrix.android.sdk.internal.database.model.RoomEntity -import org.matrix.android.sdk.internal.database.model.TimelineEventEntity -import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields -import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates -import org.matrix.android.sdk.internal.database.query.where -import org.matrix.android.sdk.internal.database.query.whereRoomId import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.sync.ReadReceiptHandler -import org.matrix.android.sdk.internal.task.TaskExecutor -import org.matrix.android.sdk.internal.task.configureWith -import org.matrix.android.sdk.internal.util.Debouncer +import org.matrix.android.sdk.internal.task.SemaphoreCoroutineSequencer import org.matrix.android.sdk.internal.util.createBackgroundHandler -import org.matrix.android.sdk.internal.util.createUIHandler import timber.log.Timber -import java.util.Collections import java.util.UUID import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference -import kotlin.math.max -private const val MIN_FETCHING_COUNT = 30 - -internal class DefaultTimeline( - private val roomId: String, - private var initialEventId: String? = null, - private val realmConfiguration: RealmConfiguration, - private val taskExecutor: TaskExecutor, - private val contextOfEventTask: GetContextOfEventTask, - private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, - private val paginationTask: PaginationTask, - private val timelineEventMapper: TimelineEventMapper, - private val settings: TimelineSettings, - private val timelineInput: TimelineInput, - private val eventDecryptor: TimelineEventDecryptor, - private val realmSessionProvider: RealmSessionProvider, - private val loadRoomMembersTask: LoadRoomMembersTask, - private val readReceiptHandler: ReadReceiptHandler -) : Timeline, - TimelineInput.Listener, - UIEchoManager.Listener { +class DefaultTimeline internal constructor(private val roomId: String, + private val initialEventId: String?, + private val realmConfiguration: RealmConfiguration, + private val loadRoomMembersTask: LoadRoomMembersTask, + private val readReceiptHandler: ReadReceiptHandler, + paginationTask: PaginationTask, + getEventTask: GetContextOfEventTask, + fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, + timelineEventMapper: TimelineEventMapper, + timelineInput: TimelineInput, + eventDecryptor: TimelineEventDecryptor) : Timeline { companion object { - val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD") + val BACKGROUND_HANDLER = createBackgroundHandler("SimpleTimeline_Thread") } - private val listeners = CopyOnWriteArrayList() - private val isStarted = AtomicBoolean(false) - private val isReady = AtomicBoolean(false) - private val mainHandler = createUIHandler() - private val backgroundRealm = AtomicReference() - private val cancelableBag = CancelableBag() - private val debouncer = Debouncer(mainHandler) - - private lateinit var timelineEvents: RealmResults - private lateinit var sendingEvents: RealmResults - - private var prevDisplayIndex: Int? = null - private var nextDisplayIndex: Int? = null - - private val uiEchoManager = UIEchoManager(settings, this) - - private val builtEvents = Collections.synchronizedList(ArrayList()) - private val builtEventsIdMap = Collections.synchronizedMap(HashMap()) - private val backwardsState = AtomicReference(TimelineState()) - private val forwardsState = AtomicReference(TimelineState()) - override val timelineID = UUID.randomUUID().toString() - override val isLive - get() = !hasMoreToLoad(Timeline.Direction.FORWARDS) + private val listeners = CopyOnWriteArrayList() + private val isStarted = AtomicBoolean(false) + private val forwardState = AtomicReference(Timeline.PaginationState()) + private val backwardState = AtomicReference(Timeline.PaginationState()) - private val eventsChangeListener = OrderedRealmCollectionChangeListener> { results, changeSet -> - if (!results.isLoaded || !results.isValid) { - return@OrderedRealmCollectionChangeListener - } - Timber.v("## SendEvent: [${System.currentTimeMillis()}] DB update for room $roomId") - handleUpdates(results, changeSet) + private val backgroundRealm = AtomicReference() + private val timelineDispatcher = BACKGROUND_HANDLER.asCoroutineDispatcher() + private val timelineScope = CoroutineScope(SupervisorJob() + timelineDispatcher) + private val sequencer = SemaphoreCoroutineSequencer() + + private val strategyDependencies = LoadTimelineStrategy.Dependencies( + eventDecryptor = eventDecryptor, + paginationTask = paginationTask, + fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, + timelineInput = timelineInput, + timelineEventMapper = timelineEventMapper, + realm = backgroundRealm, + getContextOfEventTask = getEventTask, + onEventsUpdated = this::postSnapshot + ) + private var strategy: LoadTimelineStrategy = buildStrategy(LoadTimelineStrategy.Mode.Default) + + override val isLive: Boolean + get() = !getPaginationState(Timeline.Direction.FORWARDS).hasMoreToLoad + + override fun addListener(listener: Timeline.Listener): Boolean { + listeners.add(listener) + postSnapshot() + return true } - // Public methods ****************************************************************************** + override fun removeListener(listener: Timeline.Listener): Boolean { + return listeners.remove(listener) + } + + override fun removeAllListeners() { + listeners.clear() + } + + override fun start() { + timelineScope.launch { + loadRoomMemberIfNeeded() + } + timelineScope.launch { + sequencer.post { + if (isStarted.compareAndSet(false, true)) { + val realm = Realm.getInstance(realmConfiguration) + ensureReadReceiptAreLoaded(realm) + backgroundRealm.set(realm) + openAround(initialEventId) + strategy.onStart() + } + } + } + } + + override fun dispose() { + timelineScope.coroutineContext.cancelChildren() + timelineScope.launch { + sequencer.post { + if (isStarted.compareAndSet(true, false)) { + strategy.onStop() + backgroundRealm.get().closeQuietly() + } + } + } + } + + override fun restartWithEventId(eventId: String?) { + timelineScope.launch { + openAround(eventId) + } + } + + override fun hasMoreToLoad(direction: Timeline.Direction): Boolean { + return getPaginationState(direction).hasMoreToLoad + } override fun paginate(direction: Timeline.Direction, count: Int) { - BACKGROUND_HANDLER.post { - if (!canPaginate(direction)) { - return@post - } - Timber.v("Paginate $direction of $count items") - val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex - val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, count) - if (shouldPostSnapshot) { - postSnapshot() - } + timelineScope.launch { + loadMore(count.toLong(), direction) } } override fun pendingEventCount(): Int { - return realmSessionProvider.withRealm { - RoomEntity.where(it, roomId).findFirst()?.sendingTimelineEvents?.count() ?: 0 - } + return 0 } override fun failedToDeliverEventCount(): Int { - return realmSessionProvider.withRealm { - TimelineEventEntity.findAllInRoomWithSendStates(it, roomId, SendState.HAS_FAILED_STATES).count() + return 0 + } + + override fun getIndexOfEvent(eventId: String?): Int? { + if (eventId == null) return null + return strategy.getBuiltEventIndex(eventId) + } + + override fun getTimelineEventAtIndex(index: Int): TimelineEvent? { + return null + } + + override fun getTimelineEventWithId(eventId: String?): TimelineEvent? { + if (eventId == null) return null + return strategy.getBuiltEvent(eventId) + } + + override fun getPaginationState(direction: Timeline.Direction): Timeline.PaginationState { + return if (direction == Timeline.Direction.BACKWARDS) { + backwardState + } else { + forwardState + }.get() + } + + private suspend fun loadMore(count: Long, direction: Timeline.Direction) = withContext(timelineDispatcher) { + val baseLogMessage = "loadMore(count: $count, direction: $direction, roomId: $roomId)" + Timber.v("$baseLogMessage started") + if (!isStarted.get()) { + throw IllegalStateException("You should call start before using timeline") + } + val currentState = getPaginationState(direction) + if (!currentState.hasMoreToLoad) { + Timber.v("$baseLogMessage : nothing more to load") + return@withContext + } + if (currentState.loading) { + Timber.v("$baseLogMessage : already loading") + return@withContext + } + updateState(direction) { + it.copy(loading = true) + } + val loadMoreResult = strategy.loadMore(count, direction) + val hasMoreToLoad = loadMoreResult != LoadMoreResult.REACHED_END + updateState(direction) { + it.copy(loading = false, hasMoreToLoad = hasMoreToLoad) + } + postSnapshot() + } + + private suspend fun openAround(eventId: String?) = withContext(timelineDispatcher) { + val baseLogMessage = "openAround(eventId: $eventId)" + Timber.v("$baseLogMessage started") + if (!isStarted.get()) { + throw IllegalStateException("You should call start before using timeline") + } + strategy.onStop() + strategy = if (eventId == null) { + buildStrategy(LoadTimelineStrategy.Mode.Default) + } else { + buildStrategy(LoadTimelineStrategy.Mode.Permalink(eventId)) + } + updateState(Timeline.Direction.FORWARDS) { + it.copy(loading = false, hasMoreToLoad = eventId != null) + } + updateState(Timeline.Direction.BACKWARDS) { + it.copy(loading = false, hasMoreToLoad = true) + } + strategy.onStart() + postSnapshot() + } + + private fun postSnapshot() { + timelineScope.launch { + val snapshot = strategy.buildSnapshot() + withContext(Dispatchers.Main) { + listeners.forEach { + tryOrNull { it.onTimelineUpdated(snapshot) } + } + } } } - override fun start() { - if (isStarted.compareAndSet(false, true)) { - Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId") - timelineInput.listeners.add(this) - BACKGROUND_HANDLER.post { - eventDecryptor.start() - val realm = Realm.getInstance(realmConfiguration) - backgroundRealm.set(realm) - - val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() - ?: throw IllegalStateException("Can't open a timeline without a room") - - // We don't want to filter here because some sending events that are not displayed - // are still used for ui echo (relation like reaction) - sendingEvents = roomEntity.sendingTimelineEvents.where()/*.filterEventsWithSettings()*/.findAll() - sendingEvents.addChangeListener { events -> - uiEchoManager.onSentEventsInDatabase(events.map { it.eventId }) - postSnapshot() - } - - timelineEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() - timelineEvents.addChangeListener(eventsChangeListener) - handleInitialLoad() - loadRoomMembersTask - .configureWith(LoadRoomMembersTask.Params(roomId)) - .executeBy(taskExecutor) - - // Ensure ReadReceipt from init sync are loaded - ensureReadReceiptAreLoaded(realm) - - isReady.set(true) + private suspend fun updateState(direction: Timeline.Direction, update: (Timeline.PaginationState) -> Timeline.PaginationState) { + val stateReference = when (direction) { + Timeline.Direction.FORWARDS -> forwardState + Timeline.Direction.BACKWARDS -> backwardState + } + val currentValue = stateReference.get() + val newValue = update(currentValue) + stateReference.set(newValue) + withContext(Dispatchers.Main) { + listeners.forEach { + tryOrNull { it.onStateUpdated() } } } } + private fun buildStrategy(mode: LoadTimelineStrategy.Mode): LoadTimelineStrategy { + return LoadTimelineStrategy( + roomId = roomId, + timelineId = timelineID, + mode = mode, + dependencies = strategyDependencies + ) + } + + private suspend fun loadRoomMemberIfNeeded() { + val loadRoomMembersParam = LoadRoomMembersTask.Params(roomId) + try { + loadRoomMembersTask.execute(loadRoomMembersParam) + } catch (failure: Throwable) { + Timber.v("Failed to load room members. Retry in 10s.") + delay(10_000L) + loadRoomMemberIfNeeded() + } + } + private fun ensureReadReceiptAreLoaded(realm: Realm) { readReceiptHandler.getContentFromInitSync(roomId) ?.also { @@ -190,542 +279,4 @@ internal class DefaultTimeline( } } } - - override fun dispose() { - if (isStarted.compareAndSet(true, false)) { - isReady.set(false) - timelineInput.listeners.remove(this) - Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId") - cancelableBag.cancel() - BACKGROUND_HANDLER.removeCallbacksAndMessages(null) - BACKGROUND_HANDLER.post { - if (this::sendingEvents.isInitialized) { - sendingEvents.removeAllChangeListeners() - } - if (this::timelineEvents.isInitialized) { - timelineEvents.removeAllChangeListeners() - } - clearAllValues() - backgroundRealm.getAndSet(null).also { - it?.close() - } - eventDecryptor.destroy() - } - } - } - - override fun restartWithEventId(eventId: String?) { - dispose() - initialEventId = eventId - start() - postSnapshot() - } - - override fun getTimelineEventAtIndex(index: Int): TimelineEvent? { - return builtEvents.getOrNull(index) - } - - override fun getIndexOfEvent(eventId: String?): Int? { - return builtEventsIdMap[eventId] - } - - override fun getTimelineEventWithId(eventId: String?): TimelineEvent? { - return builtEventsIdMap[eventId]?.let { - getTimelineEventAtIndex(it) - } - } - - override fun hasMoreToLoad(direction: Timeline.Direction): Boolean { - return hasMoreInCache(direction) || !hasReachedEnd(direction) - } - - override fun addListener(listener: Timeline.Listener): Boolean { - if (listeners.contains(listener)) { - return false - } - return listeners.add(listener).also { - postSnapshot() - } - } - - override fun removeListener(listener: Timeline.Listener): Boolean { - return listeners.remove(listener) - } - - override fun removeAllListeners() { - listeners.clear() - } - - override fun onNewTimelineEvents(roomId: String, eventIds: List) { - if (isLive && this.roomId == roomId) { - listeners.forEach { - it.onNewTimelineEvents(eventIds) - } - } - } - - override fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) { - if (roomId != this.roomId || !isLive) return - uiEchoManager.onLocalEchoCreated(timelineEvent) - listeners.forEach { - tryOrNull { - it.onNewTimelineEvents(listOf(timelineEvent.eventId)) - } - } - postSnapshot() - } - - override fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) { - if (roomId != this.roomId || !isLive) return - if (uiEchoManager.onSendStateUpdated(eventId, sendState)) { - postSnapshot() - } - } - - override fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean { - return tryOrNull { - builtEventsIdMap[eventId]?.let { builtIndex -> - // Update the relation of existing event - builtEvents[builtIndex]?.let { te -> - val rebuiltEvent = builder(te) - // If rebuilt event is filtered its returned as null and should be removed. - if (rebuiltEvent == null) { - builtEventsIdMap.remove(eventId) - builtEventsIdMap.entries.filter { it.value > builtIndex }.forEach { it.setValue(it.value - 1) } - builtEvents.removeAt(builtIndex) - } else { - builtEvents[builtIndex] = rebuiltEvent - } - true - } - } - } ?: false - } - -// Private methods ***************************************************************************** - - private fun hasMoreInCache(direction: Timeline.Direction) = getState(direction).hasMoreInCache - - private fun hasReachedEnd(direction: Timeline.Direction) = getState(direction).hasReachedEnd - - private fun updateLoadingStates(results: RealmResults) { - val lastCacheEvent = results.lastOrNull() - val firstCacheEvent = results.firstOrNull() - val chunkEntity = getLiveChunk() - - updateState(Timeline.Direction.FORWARDS) { - it.copy( - hasMoreInCache = !builtEventsIdMap.containsKey(firstCacheEvent?.eventId), - hasReachedEnd = chunkEntity?.isLastForward ?: false - ) - } - updateState(Timeline.Direction.BACKWARDS) { - it.copy( - hasMoreInCache = !builtEventsIdMap.containsKey(lastCacheEvent?.eventId), - hasReachedEnd = chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE - ) - } - } - - /** - * This has to be called on TimelineThread as it accesses realm live results - * @return true if createSnapshot should be posted - */ - private fun paginateInternal(startDisplayIndex: Int?, - direction: Timeline.Direction, - count: Int): Boolean { - if (count == 0) { - return false - } - updateState(direction) { it.copy(requestedPaginationCount = count, isPaginating = true) } - val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong()) - val shouldFetchMore = builtCount < count && !hasReachedEnd(direction) - if (shouldFetchMore) { - val newRequestedCount = count - builtCount - updateState(direction) { it.copy(requestedPaginationCount = newRequestedCount) } - val fetchingCount = max(MIN_FETCHING_COUNT, newRequestedCount) - executePaginationTask(direction, fetchingCount) - } else { - updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } - } - return !shouldFetchMore - } - - private fun createSnapshot(): List { - return buildSendingEvents() + builtEvents.toList() - } - - private fun buildSendingEvents(): List { - val builtSendingEvents = mutableListOf() - if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) { - uiEchoManager.getInMemorySendingEvents() - .updateWithUiEchoInto(builtSendingEvents) - sendingEvents - .filter { timelineEvent -> - builtSendingEvents.none { it.eventId == timelineEvent.eventId } - } - .map { timelineEventMapper.map(it) } - .updateWithUiEchoInto(builtSendingEvents) - } - return builtSendingEvents - } - - private fun List.updateWithUiEchoInto(target: MutableList) { - target.addAll( - // Get most up to date send state (in memory) - map { uiEchoManager.updateSentStateWithUiEcho(it) } - ) - } - - private fun canPaginate(direction: Timeline.Direction): Boolean { - return isReady.get() && !getState(direction).isPaginating && hasMoreToLoad(direction) - } - - private fun getState(direction: Timeline.Direction): TimelineState { - return when (direction) { - Timeline.Direction.FORWARDS -> forwardsState.get() - Timeline.Direction.BACKWARDS -> backwardsState.get() - } - } - - private fun updateState(direction: Timeline.Direction, update: (TimelineState) -> TimelineState) { - val stateReference = when (direction) { - Timeline.Direction.FORWARDS -> forwardsState - Timeline.Direction.BACKWARDS -> backwardsState - } - val currentValue = stateReference.get() - val newValue = update(currentValue) - stateReference.set(newValue) - } - - /** - * This has to be called on TimelineThread as it accesses realm live results - */ - private fun handleInitialLoad() { - var shouldFetchInitialEvent = false - val currentInitialEventId = initialEventId - val initialDisplayIndex = if (currentInitialEventId == null) { - timelineEvents.firstOrNull()?.displayIndex - } else { - val initialEvent = timelineEvents.where() - .equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId) - .findFirst() - - shouldFetchInitialEvent = initialEvent == null - initialEvent?.displayIndex - } - prevDisplayIndex = initialDisplayIndex - nextDisplayIndex = initialDisplayIndex - if (currentInitialEventId != null && shouldFetchInitialEvent) { - fetchEvent(currentInitialEventId) - } else { - val count = timelineEvents.size.coerceAtMost(settings.initialSize) - if (initialEventId == null) { - paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) - } else { - paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, (count / 2).coerceAtLeast(1)) - paginateInternal(initialDisplayIndex?.minus(1), Timeline.Direction.BACKWARDS, (count / 2).coerceAtLeast(1)) - } - } - postSnapshot() - } - - /** - * This has to be called on TimelineThread as it accesses realm live results - */ - private fun handleUpdates(results: RealmResults, changeSet: OrderedCollectionChangeSet) { - // If changeSet has deletion we are having a gap, so we clear everything - if (changeSet.deletionRanges.isNotEmpty()) { - clearAllValues() - } - var postSnapshot = false - changeSet.insertionRanges.forEach { range -> - val (startDisplayIndex, direction) = if (range.startIndex == 0) { - Pair(results[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS) - } else { - Pair(results[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS) - } - val state = getState(direction) - if (state.isPaginating) { - // We are getting new items from pagination - postSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedPaginationCount) - } else { - // We are getting new items from sync - buildTimelineEvents(startDisplayIndex, direction, range.length.toLong()) - postSnapshot = true - } - } - changeSet.changes.forEach { index -> - val eventEntity = results[index] - eventEntity?.eventId?.let { eventId -> - postSnapshot = rebuildEvent(eventId) { - buildTimelineEvent(eventEntity) - } || postSnapshot - } - } - if (postSnapshot) { - postSnapshot() - } - } - - /** - * This has to be called on TimelineThread as it accesses realm live results - */ - private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { - val currentChunk = getLiveChunk() - val token = if (direction == Timeline.Direction.BACKWARDS) currentChunk?.prevToken else currentChunk?.nextToken - if (token == null) { - if (direction == Timeline.Direction.BACKWARDS - || (direction == Timeline.Direction.FORWARDS && currentChunk?.hasBeenALastForwardChunk().orFalse())) { - // We are in the case where event exists, but we do not know the token. - // Fetch (again) the last event to get a token - val lastKnownEventId = if (direction == Timeline.Direction.FORWARDS) { - timelineEvents.firstOrNull()?.eventId - } else { - timelineEvents.lastOrNull()?.eventId - } - if (lastKnownEventId == null) { - updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } - } else { - val params = FetchTokenAndPaginateTask.Params( - roomId = roomId, - limit = limit, - direction = direction.toPaginationDirection(), - lastKnownEventId = lastKnownEventId - ) - cancelableBag += fetchTokenAndPaginateTask - .configureWith(params) { - this.callback = createPaginationCallback(limit, direction) - } - .executeBy(taskExecutor) - } - } else { - updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } - } - } else { - val params = PaginationTask.Params( - roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit - ) - Timber.v("Should fetch $limit items $direction") - cancelableBag += paginationTask - .configureWith(params) { - this.callback = createPaginationCallback(limit, direction) - } - .executeBy(taskExecutor) - } - } - - // For debug purpose only - private fun dumpAndLogChunks() { - val liveChunk = getLiveChunk() - Timber.w("Live chunk: $liveChunk") - - Realm.getInstance(realmConfiguration).use { realm -> - ChunkEntity.where(realm, roomId).findAll() - .also { Timber.w("Found ${it.size} chunks") } - .forEach { - Timber.w("") - Timber.w("ChunkEntity: $it") - Timber.w("prevToken: ${it.prevToken}") - Timber.w("nextToken: ${it.nextToken}") - Timber.w("isLastBackward: ${it.isLastBackward}") - Timber.w("isLastForward: ${it.isLastForward}") - it.timelineEvents.forEach { tle -> - Timber.w(" TLE: ${tle.root?.content}") - } - } - } - } - - /** - * This has to be called on TimelineThread as it accesses realm live results - */ - private fun getTokenLive(direction: Timeline.Direction): String? { - val chunkEntity = getLiveChunk() ?: return null - return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken - } - - /** - * This has to be called on TimelineThread as it accesses realm live results - * Return the current Chunk - */ - private fun getLiveChunk(): ChunkEntity? { - return timelineEvents.firstOrNull()?.chunk?.firstOrNull() - } - - /** - * This has to be called on TimelineThread as it accesses realm live results - * @return the number of items who have been added - */ - private fun buildTimelineEvents(startDisplayIndex: Int?, - direction: Timeline.Direction, - count: Long): Int { - if (count < 1 || startDisplayIndex == null) { - return 0 - } - val start = System.currentTimeMillis() - val offsetResults = getOffsetResults(startDisplayIndex, direction, count) - if (offsetResults.isEmpty()) { - return 0 - } - val offsetIndex = offsetResults.last()!!.displayIndex - if (direction == Timeline.Direction.BACKWARDS) { - prevDisplayIndex = offsetIndex - 1 - } else { - nextDisplayIndex = offsetIndex + 1 - } - offsetResults.forEach { eventEntity -> - - val timelineEvent = buildTimelineEvent(eventEntity) - val transactionId = timelineEvent.root.unsignedData?.transactionId - uiEchoManager.onSyncedEvent(transactionId) - - if (timelineEvent.isEncrypted() - && timelineEvent.root.mxDecryptionResult == null) { - timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineID)) } - } - - val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size - builtEvents.add(position, timelineEvent) - // Need to shift :/ - builtEventsIdMap.entries.filter { it.value >= position }.forEach { it.setValue(it.value + 1) } - builtEventsIdMap[eventEntity.eventId] = position - } - val time = System.currentTimeMillis() - start - Timber.v("Built ${offsetResults.size} items from db in $time ms") - // For the case where wo reach the lastForward chunk - updateLoadingStates(timelineEvents) - return offsetResults.size - } - - private fun buildTimelineEvent(eventEntity: TimelineEventEntity): TimelineEvent { - return timelineEventMapper.map( - timelineEventEntity = eventEntity, - buildReadReceipts = settings.buildReadReceipts - ).let { timelineEvent -> - // eventually enhance with ui echo? - uiEchoManager.decorateEventWithReactionUiEcho(timelineEvent) ?: timelineEvent - } - } - - /** - * This has to be called on TimelineThread as it accesses realm live results - */ - private fun getOffsetResults(startDisplayIndex: Int, - direction: Timeline.Direction, - count: Long): RealmResults { - val offsetQuery = timelineEvents.where() - if (direction == Timeline.Direction.BACKWARDS) { - offsetQuery - .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) - .lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) - } else { - offsetQuery - .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) - .greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) - } - return offsetQuery - .limit(count) - .findAll() - } - - private fun buildEventQuery(realm: Realm): RealmQuery { - return if (initialEventId == null) { - TimelineEventEntity - .whereRoomId(realm, roomId = roomId) - .equalTo(TimelineEventEntityFields.CHUNK.IS_LAST_FORWARD, true) - } else { - TimelineEventEntity - .whereRoomId(realm, roomId = roomId) - .`in`("${TimelineEventEntityFields.CHUNK.TIMELINE_EVENTS}.${TimelineEventEntityFields.EVENT_ID}", arrayOf(initialEventId)) - } - } - - private fun fetchEvent(eventId: String) { - val params = GetContextOfEventTask.Params(roomId, eventId) - cancelableBag += contextOfEventTask.configureWith(params) { - callback = object : MatrixCallback { - override fun onSuccess(data: TokenChunkEventPersistor.Result) { - postSnapshot() - } - - override fun onFailure(failure: Throwable) { - postFailure(failure) - } - } - } - .executeBy(taskExecutor) - } - - private fun postSnapshot() { - BACKGROUND_HANDLER.post { - if (isReady.get().not()) { - return@post - } - updateLoadingStates(timelineEvents) - val snapshot = createSnapshot() - val runnable = Runnable { - listeners.forEach { - it.onTimelineUpdated(snapshot) - } - } - debouncer.debounce("post_snapshot", runnable, 1) - } - } - - private fun postFailure(throwable: Throwable) { - if (isReady.get().not()) { - return - } - val runnable = Runnable { - listeners.forEach { - it.onTimelineFailure(throwable) - } - } - mainHandler.post(runnable) - } - - private fun clearAllValues() { - prevDisplayIndex = null - nextDisplayIndex = null - builtEvents.clear() - builtEventsIdMap.clear() - backwardsState.set(TimelineState()) - forwardsState.set(TimelineState()) - } - - private fun createPaginationCallback(limit: Int, direction: Timeline.Direction): MatrixCallback { - return object : MatrixCallback { - override fun onSuccess(data: TokenChunkEventPersistor.Result) { - when (data) { - TokenChunkEventPersistor.Result.SUCCESS -> { - Timber.v("Success fetching $limit items $direction from pagination request") - } - TokenChunkEventPersistor.Result.REACHED_END -> { - postSnapshot() - } - TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> - // Database won't be updated, so we force pagination request - BACKGROUND_HANDLER.post { - executePaginationTask(direction, limit) - } - } - } - - override fun onFailure(failure: Throwable) { - updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } - postSnapshot() - Timber.v("Failure fetching $limit items $direction from pagination request") - } - } - } - - // Extension methods *************************************************************************** - - private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { - return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 8de36d0427..9c64dce388 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -65,17 +65,14 @@ internal class DefaultTimelineService @AssistedInject constructor( roomId = roomId, initialEventId = eventId, realmConfiguration = monarchy.realmConfiguration, - taskExecutor = taskExecutor, - contextOfEventTask = contextOfEventTask, paginationTask = paginationTask, timelineEventMapper = timelineEventMapper, - settings = settings, timelineInput = timelineInput, eventDecryptor = eventDecryptor, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, - realmSessionProvider = realmSessionProvider, loadRoomMembersTask = loadRoomMembersTask, - readReceiptHandler = readReceiptHandler + readReceiptHandler = readReceiptHandler, + getEventTask = contextOfEventTask ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt similarity index 70% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineState.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt index 0143d9bab3..8b0cf0715b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * Copyright (c) 2021 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. @@ -16,9 +16,8 @@ package org.matrix.android.sdk.internal.session.room.timeline -internal data class TimelineState( - val hasReachedEnd: Boolean = false, - val hasMoreInCache: Boolean = true, - val isPaginating: Boolean = false, - val requestedPaginationCount: Int = 0 -) +internal enum class LoadMoreResult { + REACHED_END, + SUCCESS, + FAILURE +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt new file mode 100644 index 0000000000..ce1ef7f186 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2021 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 org.matrix.android.sdk.internal.session.room.timeline + +import io.realm.OrderedCollectionChangeSet +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm +import io.realm.RealmResults +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntityFields +import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents +import org.matrix.android.sdk.internal.database.query.where +import java.util.concurrent.atomic.AtomicReference + +internal class LoadTimelineStrategy( + private val roomId: String, + private val timelineId: String, + private val mode: Mode, + private val dependencies: Dependencies) { + + sealed class Mode { + object Default : Mode() + data class Permalink(val originEventId: String) : Mode() + + fun originEventId(): String? { + return if (this is Permalink) { + originEventId + } else { + null + } + } + } + + data class Dependencies( + val realm: AtomicReference, + val eventDecryptor: TimelineEventDecryptor, + val paginationTask: PaginationTask, + val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, + val getContextOfEventTask: GetContextOfEventTask, + val timelineInput: TimelineInput, + val timelineEventMapper: TimelineEventMapper, + val onEventsUpdated: () -> Unit + ) + + private var chunkEntity: RealmResults? = null + private var timelineChunk: TimelineChunk? = null + + private val chunkEntityListener = OrderedRealmCollectionChangeListener { _: RealmResults, changeSet: OrderedCollectionChangeSet -> + val shouldRebuildChunk = changeSet.insertions.isNotEmpty() + if (shouldRebuildChunk) { + timelineChunk?.close(closeNext = true, closePrev = true) + timelineChunk = chunkEntity?.createTimelineChunk() + dependencies.onEventsUpdated() + } + } + + private val uiEchoManagerListener = object : UIEchoManager.Listener { + override fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean { + return timelineChunk?.rebuildEvent(eventId, builder, searchInNext = true, searchInPrev = true).orFalse() + } + } + + private val timelineInputListener = object : TimelineInput.Listener { + override fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) { + if (roomId != this@LoadTimelineStrategy.roomId) { + return + } + if (uiEchoManager.onLocalEchoCreated(timelineEvent)) { + dependencies.onEventsUpdated() + } + } + + override fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) { + if (roomId != this@LoadTimelineStrategy.roomId) { + return + } + if (uiEchoManager.onSendStateUpdated(eventId, sendState)) { + dependencies.onEventsUpdated() + } + } + } + + private val uiEchoManager = UIEchoManager(TimelineSettings(10), uiEchoManagerListener) + private val sendingEventsDataSource: SendingEventsDataSource = RealmSendingEventsDataSource( + roomId = roomId, + realm = dependencies.realm, + uiEchoManager = uiEchoManager, + timelineEventMapper = dependencies.timelineEventMapper, + onEventsUpdated = dependencies.onEventsUpdated + ) + + suspend fun onStart() { + dependencies.eventDecryptor.start() + dependencies.timelineInput.listeners.add(timelineInputListener) + val realm = dependencies.realm.get() + sendingEventsDataSource.start() + chunkEntity = getChunkEntity(realm).also { + it.addChangeListener(chunkEntityListener) + timelineChunk = it.createTimelineChunk() + } + if(mode is Mode.Default){ + loadMore(10, Timeline.Direction.BACKWARDS) + } + } + + fun onStop() { + dependencies.eventDecryptor.destroy() + dependencies.timelineInput.listeners.remove(timelineInputListener) + chunkEntity?.removeChangeListener(chunkEntityListener) + sendingEventsDataSource.stop() + timelineChunk?.close(closeNext = true, closePrev = true) + chunkEntity = null + timelineChunk = null + } + + suspend fun loadMore(count: Long, direction: Timeline.Direction): LoadMoreResult { + return if (mode is Mode.Permalink && timelineChunk == null) { + val params = GetContextOfEventTask.Params(roomId, mode.originEventId) + try { + dependencies.getContextOfEventTask.execute(params) + LoadMoreResult.SUCCESS + } catch (failure: Throwable) { + LoadMoreResult.FAILURE + } + } else { + timelineChunk?.loadMore(count, direction) ?: LoadMoreResult.FAILURE + } + } + + fun getBuiltEventIndex(eventId: String): Int? { + return timelineChunk?.getBuiltEventIndex(eventId, searchInNext = true, searchInPrev = true) + } + + fun getBuiltEvent(eventId: String): TimelineEvent? { + return timelineChunk?.getBuiltEvent(eventId, searchInNext = true, searchInPrev = true) + } + + fun buildSnapshot(): List { + return buildSendingEvents() + timelineChunk?.builtItems(includesNext = true, includesPrev = true).orEmpty() + } + + private fun buildSendingEvents(): List { + return if (timelineChunk?.hasReachedLastForward().orFalse()) { + sendingEventsDataSource.buildSendingEvents() + } else { + emptyList() + } + } + + private fun getChunkEntity(realm: Realm): RealmResults { + return if (mode is Mode.Permalink) { + ChunkEntity.findAllIncludingEvents(realm, listOf(mode.originEventId)) + } else { + ChunkEntity.where(realm, roomId) + .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true) + .findAll() + } + } + + private fun RealmResults.createTimelineChunk(): TimelineChunk? { + return firstOrNull()?.let { + return TimelineChunk( + chunkEntity = it, + roomId = roomId, + timelineId = timelineId, + eventDecryptor = dependencies.eventDecryptor, + paginationTask = dependencies.paginationTask, + fetchTokenAndPaginateTask = dependencies.fetchTokenAndPaginateTask, + timelineEventMapper = dependencies.timelineEventMapper, + uiEchoManager = uiEchoManager, + initialEventId = mode.originEventId(), + onBuiltEvents = dependencies.onEventsUpdated + ) + } + } +} + + diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt new file mode 100644 index 0000000000..58298609a0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2021 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 org.matrix.android.sdk.internal.session.room.timeline + +import io.realm.Realm +import io.realm.RealmChangeListener +import io.realm.RealmList +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.where +import java.util.concurrent.atomic.AtomicReference + +internal interface SendingEventsDataSource { + fun start() + fun stop() + fun buildSendingEvents(): List +} + +internal class RealmSendingEventsDataSource( + private val roomId: String, + private val realm: AtomicReference, + private val uiEchoManager: UIEchoManager, + private val timelineEventMapper: TimelineEventMapper, + private val onEventsUpdated: () -> Unit +) : SendingEventsDataSource { + + private var roomEntity: RoomEntity? = null + private var sendingTimelineEvents: RealmList? = null + + private val sendingTimelineEventsListener = RealmChangeListener> { events -> + uiEchoManager.onSentEventsInDatabase(events.map { it.eventId }) + onEventsUpdated() + } + + override fun start() { + val safeRealm = realm.get() + roomEntity = RoomEntity.where(safeRealm, roomId = roomId).findFirst() + sendingTimelineEvents = roomEntity?.sendingTimelineEvents + sendingTimelineEvents?.addChangeListener(sendingTimelineEventsListener) + } + + override fun stop() { + sendingTimelineEvents?.removeChangeListener(sendingTimelineEventsListener) + sendingTimelineEvents = null + roomEntity = null + } + + override fun buildSendingEvents(): List { + val builtSendingEvents = mutableListOf() + uiEchoManager.getInMemorySendingEvents() + .addWithUiEcho(builtSendingEvents) + sendingTimelineEvents?.freeze() + ?.filter { timelineEvent -> + builtSendingEvents.none { it.eventId == timelineEvent.eventId } + } + ?.map { + timelineEventMapper.map(it) + }?.addWithUiEcho(builtSendingEvents) + + return builtSendingEvents + } + + private fun List.addWithUiEcho(target: MutableList) { + target.addAll( + map { uiEchoManager.updateSentStateWithUiEcho(it) } + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt new file mode 100644 index 0000000000..6c337b3663 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2021 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 org.matrix.android.sdk.internal.session.room.timeline + +import io.realm.OrderedCollectionChangeSet +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.RealmObjectChangeListener +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.Sort +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields +import timber.log.Timber +import java.util.Collections + +/** + * This is the value used to fetch on server. It's better to make constant as otherwise we can have weird chunks with disparate and small chunk of data. + */ +private const val PAGINATION_COUNT = 50 + +internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, + private val roomId: String, + private val timelineId: String, + private val eventDecryptor: TimelineEventDecryptor, + private val paginationTask: PaginationTask, + private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, + private val timelineEventMapper: TimelineEventMapper, + private val uiEchoManager: UIEchoManager? = null, + private val initialEventId: String?, + private val onBuiltEvents: () -> Unit) { + + private val chunkObjectListener = RealmObjectChangeListener { _, changeSet -> + Timber.v("on chunk (${chunkEntity.identifier()}) changed: ${changeSet?.changedFields?.joinToString(",")}") + } + + private val timelineEventCollectionListener = OrderedRealmCollectionChangeListener { results: RealmResults, changeSet: OrderedCollectionChangeSet -> + val frozenResults = results.freeze() + Timber.v("on timeline event changed: $changeSet") + handleChangeSet(frozenResults, changeSet) + } + + private var timelineEventEntities: RealmResults = chunkEntity.sortedTimelineEvents() + private val builtEvents: MutableList = Collections.synchronizedList(ArrayList()) + private val builtEventsIndexes: MutableMap = Collections.synchronizedMap(HashMap()) + + private var nextChunk: TimelineChunk? = null + private var prevChunk: TimelineChunk? = null + + init { + timelineEventEntities.addChangeListener(timelineEventCollectionListener) + chunkEntity.addChangeListener(chunkObjectListener) + } + + fun hasReachedLastForward(): Boolean { + return if (chunkEntity.isLastForward) { + true + } else { + nextChunk?.hasReachedLastForward().orFalse() + } + } + + fun builtItems(includesNext: Boolean, includesPrev: Boolean): List { + val deepBuiltItems = ArrayList(builtEvents.size) + if (includesNext) { + val nextEvents = nextChunk?.builtItems(includesNext = true, includesPrev = false).orEmpty() + deepBuiltItems.addAll(nextEvents) + } + deepBuiltItems.addAll(builtEvents) + if (includesPrev) { + val prevEvents = prevChunk?.builtItems(includesNext = false, includesPrev = true).orEmpty() + deepBuiltItems.addAll(prevEvents) + } + return deepBuiltItems + } + + suspend fun loadMore(count: Long, direction: Timeline.Direction): LoadMoreResult { + val loadFromDbCount = loadFromDb(count, direction) + val offsetCount = count - loadFromDbCount + // We have built the right amount of data + if (offsetCount == 0L) { + onBuiltEvents() + return LoadMoreResult.SUCCESS + } + return if (direction == Timeline.Direction.FORWARDS) { + val nextChunkEntity = chunkEntity.nextChunk + if (nextChunkEntity == null) { + val token = chunkEntity.nextToken ?: return LoadMoreResult.REACHED_END // TODO handle previous live chunk + try { + fetchFromServer(token, direction) + } catch (failure: Throwable) { + Timber.v("Failed to fetch from server: $failure") + LoadMoreResult.FAILURE + } + } else { + // otherwise we delegate to the next chunk + if (nextChunk == null) { + nextChunk = createTimelineChunk(nextChunkEntity) + } + nextChunk?.loadMore(offsetCount, direction) ?: LoadMoreResult.FAILURE + } + } else { + val prevChunkEntity = chunkEntity.prevChunk + if (prevChunkEntity == null) { + val token = chunkEntity.prevToken ?: return LoadMoreResult.REACHED_END + try { + fetchFromServer(token, direction) + } catch (failure: Throwable) { + Timber.v("Failed to fetch from server: $failure") + LoadMoreResult.FAILURE + } + } else { + // otherwise we delegate to the prev chunk + if (prevChunk == null) { + prevChunk = createTimelineChunk(prevChunkEntity) + } + prevChunk?.loadMore(offsetCount, direction) ?: LoadMoreResult.FAILURE + } + } + } + + private fun loadFromDb(count: Long, direction: Timeline.Direction): Long { + val displayIndex = getNextDisplayIndex(direction) ?: return 0 + val baseQuery = timelineEventEntities.where() + val timelineEvents = baseQuery.offsets(direction, count, displayIndex).findAll().orEmpty() + if (timelineEvents.isEmpty()) return 0 + if (direction == Timeline.Direction.FORWARDS) { + builtEventsIndexes.entries.forEach { it.setValue(it.value + timelineEvents.size) } + } + timelineEvents + .mapIndexed { index, timelineEventEntity -> + val timelineEvent = timelineEventEntity.buildAndDecryptIfNeeded() + if (direction == Timeline.Direction.FORWARDS) { + builtEventsIndexes[timelineEvent.eventId] = index + builtEvents.add(index, timelineEvent) + } else { + builtEventsIndexes[timelineEvent.eventId] = builtEvents.size + builtEvents.add(timelineEvent) + } + } + return timelineEvents.size.toLong() + } + + private fun TimelineEventEntity.buildAndDecryptIfNeeded(): TimelineEvent { + val timelineEvent = buildTimelineEvent(this) + val transactionId = timelineEvent.root.unsignedData?.transactionId + uiEchoManager?.onSyncedEvent(transactionId) + if (timelineEvent.isEncrypted() + && timelineEvent.root.mxDecryptionResult == null) { + timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) } + } + return timelineEvent + } + + private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map( + timelineEventEntity = eventEntity + ).let { + // eventually enhance with ui echo? + (uiEchoManager?.decorateEventWithReactionUiEcho(it) ?: it) + } + + private fun createTimelineChunk(chunkEntity: ChunkEntity): TimelineChunk { + return TimelineChunk( + chunkEntity = chunkEntity, + timelineId = timelineId, + eventDecryptor = eventDecryptor, + roomId = roomId, + paginationTask = paginationTask, + fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, + timelineEventMapper = timelineEventMapper, + uiEchoManager = uiEchoManager, + initialEventId = null, + onBuiltEvents = onBuiltEvents + ) + } + + private suspend fun fetchFromServer(token: String, direction: Timeline.Direction): LoadMoreResult { + val paginationParams = PaginationTask.Params(roomId, token, direction.toPaginationDirection(), PAGINATION_COUNT) + val paginationResult = paginationTask.execute(paginationParams) + return when (paginationResult) { + TokenChunkEventPersistor.Result.REACHED_END -> LoadMoreResult.REACHED_END + TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE, + TokenChunkEventPersistor.Result.SUCCESS -> LoadMoreResult.SUCCESS + } + } + + fun getBuiltEventIndex(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): Int? { + val builtEventIndex = builtEventsIndexes[eventId] + if (builtEventIndex != null) { + return getOffsetIndex() + builtEventIndex + } + if (searchInNext) { + val nextBuiltEventIndex = nextChunk?.getBuiltEventIndex(eventId, searchInNext = true, searchInPrev = false) + if (nextBuiltEventIndex != null) { + return nextBuiltEventIndex + } + } + if (searchInPrev) { + val prevBuiltEventIndex = prevChunk?.getBuiltEventIndex(eventId, searchInNext = false, searchInPrev = true) + if (prevBuiltEventIndex != null) { + return prevBuiltEventIndex + } + } + return null + } + + fun getBuiltEvent(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): TimelineEvent? { + val builtEventIndex = builtEventsIndexes[eventId] + if (builtEventIndex != null) { + return builtEvents.getOrNull(builtEventIndex) + } + if (searchInNext) { + val nextBuiltEvent = nextChunk?.getBuiltEvent(eventId, searchInNext = true, searchInPrev = false) + if (nextBuiltEvent != null) { + return nextBuiltEvent + } + } + if (searchInPrev) { + val prevBuiltEvent = prevChunk?.getBuiltEvent(eventId, searchInNext = false, searchInPrev = true) + if (prevBuiltEvent != null) { + return prevBuiltEvent + } + } + return null + } + + private fun getOffsetIndex(): Int { + var offset = 0 + var currentNextChunk = nextChunk + while (currentNextChunk != null) { + offset += currentNextChunk.builtEvents.size + currentNextChunk = currentNextChunk.nextChunk + } + return offset + } + + private fun handleChangeSet(frozenResults: RealmResults, changeSet: OrderedCollectionChangeSet) { + val insertions = changeSet.insertionRanges + for (range in insertions) { + val newItems = frozenResults + .subList(range.startIndex, range.startIndex + range.length) + .map { it.buildAndDecryptIfNeeded() } + builtEventsIndexes.entries.filter { it.value >= range.startIndex }.forEach { it.setValue(it.value + range.length) } + newItems.mapIndexed { index, timelineEvent -> + val correctedIndex = range.startIndex + index + builtEvents.add(correctedIndex, timelineEvent) + builtEventsIndexes[timelineEvent.eventId] = correctedIndex + } + } + val modifications = changeSet.changeRanges + for (range in modifications) { + for (modificationIndex in (range.startIndex until range.startIndex + range.length)) { + val updatedEntity = frozenResults[modificationIndex] ?: continue + try { + builtEvents[modificationIndex] = updatedEntity.buildAndDecryptIfNeeded() + } catch (failure: Throwable) { + Timber.v("Fail to update items at index: $modificationIndex") + } + } + } + if (insertions.isNotEmpty() || modifications.isNotEmpty()) { + onBuiltEvents() + } + } + + fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?, searchInNext: Boolean, searchInPrev: Boolean): Boolean { + return tryOrNull { + val builtIndex = getBuiltEventIndex(eventId, searchInNext = false, searchInPrev = false) + if (builtIndex == null) { + val foundInPrev = searchInPrev && prevChunk?.rebuildEvent(eventId, builder, searchInNext = false, searchInPrev = true).orFalse() + if (foundInPrev) { + return true + } + if (searchInNext) { + return prevChunk?.rebuildEvent(eventId, builder, searchInPrev = false, searchInNext = true).orFalse() + } + return false + } + // Update the relation of existing event + builtEvents.getOrNull(builtIndex)?.let { te -> + val rebuiltEvent = builder(te) + builtEvents[builtIndex] = rebuiltEvent!! + true + } + } + ?: false + } + + fun close(closeNext: Boolean, closePrev: Boolean) { + if (closeNext) { + nextChunk?.close(closeNext = true, closePrev = false) + } + if (closePrev) { + prevChunk?.close(closeNext = false, closePrev = true) + } + nextChunk = null + prevChunk = null + chunkEntity.removeChangeListener(chunkObjectListener) + timelineEventEntities.removeChangeListener(timelineEventCollectionListener) + } + + private fun getNextDisplayIndex(direction: Timeline.Direction): Int? { + val frozenTimelineEvents = timelineEventEntities.freeze() + if (frozenTimelineEvents.isEmpty()) { + return null + } + return if (builtEvents.isEmpty()) { + if (initialEventId != null) { + frozenTimelineEvents.where().equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId).findFirst()?.displayIndex + } else if (direction == Timeline.Direction.BACKWARDS) { + frozenTimelineEvents.first()?.displayIndex + } else { + frozenTimelineEvents.last()?.displayIndex + } + } else if (direction == Timeline.Direction.FORWARDS) { + builtEvents.first().displayIndex + 1 + } else { + builtEvents.last().displayIndex - 1 + } + } +} + +private fun RealmQuery.offsets( + direction: Timeline.Direction, + count: Long, + startDisplayIndex: Int +): RealmQuery { + sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) + if (direction == Timeline.Direction.BACKWARDS) { + lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) + } else { + greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) + } + return limit(count) +} + +private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { + return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS +} + +private fun ChunkEntity.sortedTimelineEvents(): RealmResults { + return timelineEvents.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt index a7cba2fe99..13704a5168 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -25,22 +25,16 @@ import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.database.helper.addIfNecessary import org.matrix.android.sdk.internal.database.helper.addStateEvent import org.matrix.android.sdk.internal.database.helper.addTimelineEvent -import org.matrix.android.sdk.internal.database.helper.merge import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.RoomEntity -import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity -import org.matrix.android.sdk.internal.database.model.deleteOnCascade +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore import org.matrix.android.sdk.internal.database.query.create import org.matrix.android.sdk.internal.database.query.find -import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents -import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom -import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryEventsHelper import org.matrix.android.sdk.internal.util.awaitTransaction import timber.log.Timber import javax.inject.Inject @@ -136,21 +130,21 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri prevToken = receivedChunk.end } + val existingChunk = ChunkEntity.find(realm, roomId, prevToken = prevToken, nextToken = nextToken) + if (existingChunk != null) { + Timber.v("This chunk is already in the db, returns") + return@awaitTransaction + } val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken) val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken) - - // The current chunk is the one we will keep all along the merge processChanges. - // We try to look for a chunk next to the token, - // otherwise we create a whole new one which is unlinked (not live) - val currentChunk = if (direction == PaginationDirection.FORWARDS) { - prevChunk?.apply { this.nextToken = nextToken } - } else { - nextChunk?.apply { this.prevToken = prevToken } + val currentChunk = ChunkEntity.create(realm, prevToken = prevToken, nextToken = nextToken).apply { + this.nextChunk = nextChunk + this.prevChunk = prevChunk } - ?: ChunkEntity.create(realm, prevToken, nextToken) - - if (receivedChunk.events.isNullOrEmpty() && !receivedChunk.hasMore()) { - handleReachEnd(realm, roomId, direction, currentChunk) + nextChunk?.prevChunk = currentChunk + prevChunk?.nextChunk = currentChunk + if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) { + handleReachEnd(roomId, direction, currentChunk) } else { handlePagination(realm, roomId, direction, receivedChunk, currentChunk) } @@ -166,17 +160,10 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri } } - private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) { + private fun handleReachEnd(roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) { Timber.v("Reach end of $roomId") if (direction == PaginationDirection.FORWARDS) { - val currentLastForwardChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) - if (currentChunk != currentLastForwardChunk) { - currentChunk.isLastForward = true - currentLastForwardChunk?.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = false) - RoomSummaryEntity.where(realm, roomId).findFirst()?.apply { - latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) - } - } + Timber.v("We should keep the lastForward chunk unique, the one from sync") } else { currentChunk.isLastBackward = true } @@ -209,6 +196,21 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri if (event.eventId == null || event.senderId == null) { return@forEach } + //We check for the timeline event with this id + val eventId = event.eventId + val existingTimelineEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst() + // If it exists, we want to skip here + val existingChunk = existingTimelineEvent?.chunk?.firstOrNull() + if (existingChunk != null) { + if (direction == PaginationDirection.BACKWARDS) { + currentChunk.prevChunk = existingChunk + existingChunk.nextChunk = currentChunk + } else if (direction == PaginationDirection.FORWARDS) { + currentChunk.nextChunk = existingChunk + existingChunk.prevChunk = currentChunk + } + return@forEach + } val ageLocalTs = event.unsignedData?.age?.let { now - it } eventIds.add(event.eventId) val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) @@ -220,29 +222,8 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri } roomMemberContentsByUser[event.stateKey] = contentToUse.toModel() } - currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser) } - // Find all the chunks which contain at least one event from the list of eventIds - val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds) - Timber.d("Found ${chunks.size} chunks containing at least one of the eventIds") - val chunksToDelete = ArrayList() - chunks.forEach { - if (it != currentChunk) { - Timber.d("Merge $it") - currentChunk.merge(roomId, it, direction) - chunksToDelete.add(it) - } - } - chunksToDelete.forEach { - it.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = false) - } - val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) - val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null - || (chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS) - if (shouldUpdateSummary) { - roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) - } if (currentChunk.isValid) { RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt index 4804fbd731..25ff17dd70 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt @@ -70,13 +70,12 @@ internal class UIEchoManager( return existingState != sendState } - fun onLocalEchoCreated(timelineEvent: TimelineEvent) { - // Manage some ui echos (do it before filter because actual event could be filtered out) + fun onLocalEchoCreated(timelineEvent: TimelineEvent): Boolean { when (timelineEvent.root.getClearType()) { EventType.REDACTION -> { } EventType.REACTION -> { - val content = timelineEvent.root.content?.toModel() + val content: ReactionContent? = timelineEvent.root.content?.toModel() if (RelationType.ANNOTATION == content?.relatesTo?.type) { val reaction = content.relatesTo.key val relatedEventID = content.relatesTo.eventId @@ -96,11 +95,12 @@ internal class UIEchoManager( } Timber.v("On local echo created: ${timelineEvent.eventId}") inMemorySendingEvents.add(0, timelineEvent) + return true } - fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent? { + fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent { val relatedEventID = timelineEvent.eventId - val contents = inMemoryReactions[relatedEventID] ?: return null + val contents = inMemoryReactions[relatedEventID] ?: return timelineEvent var existingAnnotationSummary = timelineEvent.annotations ?: EventAnnotationsSummary( relatedEventID diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 8be319f2a8..e272ee86c9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -39,13 +39,13 @@ import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItem import im.vector.app.features.home.room.detail.timeline.factory.ReadReceiptsItemFactory import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams -import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroups import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener +import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroups import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem @@ -244,17 +244,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec interceptorHelper.intercept(models, partialState.unreadState, timeline, callback) } - fun update(viewState: RoomDetailViewState) = synchronized(modelCache) { + fun update(viewState: RoomDetailViewState) { val newPartialState = PartialState(viewState) - if (partialState.highlightedEventId != newPartialState.highlightedEventId) { - // Clear cache to force a refresh - for (i in 0 until modelCache.size) { - if (modelCache[i]?.eventId == viewState.highlightedEventId - || modelCache[i]?.eventId == partialState.highlightedEventId) { - modelCache[i] = null - } - } - } if (newPartialState != partialState) { partialState = newPartialState requestModelBuild() @@ -394,7 +385,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec it.id(event.localId) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } - val isCacheable = eventModel is ItemWithEvents && eventModel.isCacheable() + val isCacheable = eventModel is ItemWithEvents && eventModel.isCacheable() && !params.isHighlighted return CacheItemData( localId = event.localId, eventId = event.root.eventId, From 4f145e365e721830e466128937d0dca7c6e1948d Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 8 Sep 2021 15:33:40 +0200 Subject: [PATCH 02/29] Timeline: small on fixes on new implementation --- .../internal/session/room/timeline/DefaultTimeline.kt | 1 - .../room/detail/timeline/TimelineEventController.kt | 11 ++++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 141d33ad0e..b102ab9fd7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -109,7 +109,6 @@ class DefaultTimeline internal constructor(private val roomId: String, ensureReadReceiptAreLoaded(realm) backgroundRealm.set(realm) openAround(initialEventId) - strategy.onStart() } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index e272ee86c9..923404ac5e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -58,6 +58,7 @@ import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.settings.VectorPreferences +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel @@ -355,7 +356,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId) } // Should be build if not cached or if model should be refreshed - if (modelCache[position] == null || modelCache[position]?.isCacheable == false) { + if (modelCache[position] == null || modelCache[position]?.isCacheable(partialState).orFalse()) { val timelineEventsGroup = timelineEventsGroups.getOrNull(event) val params = TimelineItemFactoryParams( event = event, @@ -537,6 +538,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val eventModel: EpoxyModel<*>? = null, val mergedHeaderModel: BasedMergedItem<*>? = null, val formattedDayModel: DaySeparatorItem? = null, - val isCacheable: Boolean = true - ) + private val isCacheable: Boolean = true + ) { + fun isCacheable(partialState: PartialState): Boolean { + return isCacheable || partialState.highlightedEventId == eventId + } + } } From 94a69503949d16a8790cbc14090ebfc1af00041a Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 8 Sep 2021 18:00:54 +0200 Subject: [PATCH 03/29] Timeline rework: continue branching things. --- .../sdk/api/session/room/timeline/Timeline.kt | 4 +-- .../session/room/timeline/DefaultTimeline.kt | 36 ++++++++++++------- .../session/room/timeline/LoadMoreResult.kt | 2 +- .../room/timeline/LoadTimelineStrategy.kt | 18 +++++++--- .../room/timeline/SendingEventsDataSource.kt | 2 +- .../session/room/timeline/TimelineChunk.kt | 2 +- .../session/room/timeline/UIEchoManager.kt | 5 +-- 7 files changed, 41 insertions(+), 28 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt index dff45c0d94..ae202da979 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt @@ -115,9 +115,7 @@ interface Timeline { */ fun onNewTimelineEvents(eventIds: List) - fun onStateUpdated() { - //NOOP - } + fun onStateUpdated() = Unit } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index b102ab9fd7..ca86771f9b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,17 +41,17 @@ import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference -class DefaultTimeline internal constructor(private val roomId: String, - private val initialEventId: String?, - private val realmConfiguration: RealmConfiguration, - private val loadRoomMembersTask: LoadRoomMembersTask, - private val readReceiptHandler: ReadReceiptHandler, - paginationTask: PaginationTask, - getEventTask: GetContextOfEventTask, - fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, - timelineEventMapper: TimelineEventMapper, - timelineInput: TimelineInput, - eventDecryptor: TimelineEventDecryptor) : Timeline { +internal class DefaultTimeline internal constructor(private val roomId: String, + private val initialEventId: String?, + private val realmConfiguration: RealmConfiguration, + private val loadRoomMembersTask: LoadRoomMembersTask, + private val readReceiptHandler: ReadReceiptHandler, + paginationTask: PaginationTask, + getEventTask: GetContextOfEventTask, + fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, + timelineEventMapper: TimelineEventMapper, + timelineInput: TimelineInput, + eventDecryptor: TimelineEventDecryptor) : Timeline { companion object { val BACKGROUND_HANDLER = createBackgroundHandler("SimpleTimeline_Thread") @@ -77,8 +77,10 @@ class DefaultTimeline internal constructor(private val roomId: String, timelineEventMapper = timelineEventMapper, realm = backgroundRealm, getContextOfEventTask = getEventTask, - onEventsUpdated = this::postSnapshot + onEventsUpdated = this::postSnapshot, + onNewTimelineEvents = this::onNewTimelineEvents ) + private var strategy: LoadTimelineStrategy = buildStrategy(LoadTimelineStrategy.Mode.Default) override val isLive: Boolean @@ -231,6 +233,14 @@ class DefaultTimeline internal constructor(private val roomId: String, } } + private fun onNewTimelineEvents(eventIds: List) { + timelineScope.launch(Dispatchers.Main) { + listeners.forEach { + tryOrNull { it.onNewTimelineEvents(eventIds) } + } + } + } + private suspend fun updateState(direction: Timeline.Direction, update: (Timeline.PaginationState) -> Timeline.PaginationState) { val stateReference = when (direction) { Timeline.Direction.FORWARDS -> forwardState diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt index 8b0cf0715b..c419e8325e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadMoreResult.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index ce1ef7f186..0c8a1ae903 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ChunkEntityFields @@ -59,7 +58,8 @@ internal class LoadTimelineStrategy( val getContextOfEventTask: GetContextOfEventTask, val timelineInput: TimelineInput, val timelineEventMapper: TimelineEventMapper, - val onEventsUpdated: () -> Unit + val onEventsUpdated: () -> Unit, + val onNewTimelineEvents: (List) -> Unit ) private var chunkEntity: RealmResults? = null @@ -86,6 +86,7 @@ internal class LoadTimelineStrategy( return } if (uiEchoManager.onLocalEchoCreated(timelineEvent)) { + dependencies.onNewTimelineEvents(listOf(timelineEvent.eventId)) dependencies.onEventsUpdated() } } @@ -98,9 +99,16 @@ internal class LoadTimelineStrategy( dependencies.onEventsUpdated() } } + + override fun onNewTimelineEvents(roomId: String, eventIds: List) { + super.onNewTimelineEvents(roomId, eventIds) + if (mode == Mode.Default && roomId == this@LoadTimelineStrategy.roomId) { + dependencies.onNewTimelineEvents(eventIds) + } + } } - private val uiEchoManager = UIEchoManager(TimelineSettings(10), uiEchoManagerListener) + private val uiEchoManager = UIEchoManager(uiEchoManagerListener) private val sendingEventsDataSource: SendingEventsDataSource = RealmSendingEventsDataSource( roomId = roomId, realm = dependencies.realm, @@ -118,7 +126,7 @@ internal class LoadTimelineStrategy( it.addChangeListener(chunkEntityListener) timelineChunk = it.createTimelineChunk() } - if(mode is Mode.Default){ + if (mode is Mode.Default) { loadMore(10, Timeline.Direction.BACKWARDS) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt index 58298609a0..c6d10a23fb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index 6c337b3663..c9f19f0ddd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 New Vector Ltd + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt index 25ff17dd70..5b2cf001f3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt @@ -28,10 +28,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import timber.log.Timber import java.util.Collections -internal class UIEchoManager( - private val settings: TimelineSettings, - private val listener: Listener -) { +internal class UIEchoManager(private val listener: Listener) { interface Listener { fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?): Boolean From da75642b92e9c7f6d5616d7b2126e6cbf3a74231 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 17 Sep 2021 15:13:38 +0200 Subject: [PATCH 04/29] Timeline: add some logs and fix epoxy cache --- .../session/room/timeline/TimelineInput.kt | 5 +++-- .../timeline/TimelineEventController.kt | 22 ++++++++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineInput.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineInput.kt index cdc85ea722..a953db0704 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineInput.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineInput.kt @@ -23,6 +23,9 @@ import javax.inject.Inject @SessionScope internal class TimelineInput @Inject constructor() { + + val listeners = mutableSetOf() + fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) { listeners.toSet().forEach { it.onLocalEchoCreated(roomId, timelineEvent) } } @@ -35,8 +38,6 @@ internal class TimelineInput @Inject constructor() { listeners.toSet().forEach { it.onNewTimelineEvents(roomId, eventIds) } } - val listeners = mutableSetOf() - internal interface Listener { fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) = Unit fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) = Unit diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 923404ac5e..377e5e06df 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -58,7 +58,6 @@ import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.settings.VectorPreferences -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel @@ -71,7 +70,9 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoCon import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import timber.log.Timber import javax.inject.Inject +import kotlin.system.measureTimeMillis class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, private val vectorPreferences: VectorPreferences, @@ -321,7 +322,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } private fun getModels(): List> { - buildCacheItemsIfNeeded() + val timeForBuilding = measureTimeMillis { + buildCacheItemsIfNeeded() + } + Timber.v("Time for building cache items: $timeForBuilding ms") return modelCache .map { cacheItemData -> val eventModel = if (cacheItemData == null || mergedHeaderItemFactory.isCollapsed(cacheItemData.localId)) { @@ -346,7 +350,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec if (modelCache.isEmpty()) { return } - preprocessReverseEvents() + val preprocessEventsTiming = measureTimeMillis { + preprocessReverseEvents() + } + Timber.v("Preprocess events took $preprocessEventsTiming ms") + var numberOfEventsToBuild = 0 val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvent) (0 until modelCache.size).forEach { position -> val event = currentSnapshot[position] @@ -356,7 +364,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId) } // Should be build if not cached or if model should be refreshed - if (modelCache[position] == null || modelCache[position]?.isCacheable(partialState).orFalse()) { + if (modelCache[position] == null || modelCache[position]?.isCacheable(partialState) == false) { val timelineEventsGroup = timelineEventsGroups.getOrNull(event) val params = TimelineItemFactoryParams( event = event, @@ -369,11 +377,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec eventsGroup = timelineEventsGroup ) modelCache[position] = buildCacheItem(params) + numberOfEventsToBuild++ } val itemCachedData = modelCache[position] ?: return@forEach // Then update with additional models if needed modelCache[position] = itemCachedData.enrichWithModels(event, nextEvent, position, receiptsByEvent) } + Timber.v("Number of events to rebuild: $numberOfEventsToBuild on ${modelCache.size} total events") } private fun buildCacheItem(params: TimelineItemFactoryParams): CacheItemData { @@ -386,7 +396,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec it.id(event.localId) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } - val isCacheable = eventModel is ItemWithEvents && eventModel.isCacheable() && !params.isHighlighted + val isCacheable = (eventModel !is ItemWithEvents || eventModel.isCacheable()) && !params.isHighlighted return CacheItemData( localId = event.localId, eventId = event.root.eventId, @@ -541,7 +551,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val isCacheable: Boolean = true ) { fun isCacheable(partialState: PartialState): Boolean { - return isCacheable || partialState.highlightedEventId == eventId + return isCacheable && partialState.highlightedEventId != eventId } } } From 2283030c9b67dd0518e08f29da4e5558e12bbf3f Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 17 Sep 2021 17:51:40 +0200 Subject: [PATCH 05/29] Timeline rework: handle lastForwardChunk --- .../session/room/timeline/TimelineChunk.kt | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index c9f19f0ddd..45c74fa446 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -104,13 +104,9 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, return if (direction == Timeline.Direction.FORWARDS) { val nextChunkEntity = chunkEntity.nextChunk if (nextChunkEntity == null) { - val token = chunkEntity.nextToken ?: return LoadMoreResult.REACHED_END // TODO handle previous live chunk - try { - fetchFromServer(token, direction) - } catch (failure: Throwable) { - Timber.v("Failed to fetch from server: $failure") - LoadMoreResult.FAILURE - } + // Fetch next chunk from server if not in the db + val token = chunkEntity.nextToken + fetchFromServer(token, direction) } else { // otherwise we delegate to the next chunk if (nextChunk == null) { @@ -121,13 +117,9 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, } else { val prevChunkEntity = chunkEntity.prevChunk if (prevChunkEntity == null) { - val token = chunkEntity.prevToken ?: return LoadMoreResult.REACHED_END - try { - fetchFromServer(token, direction) - } catch (failure: Throwable) { - Timber.v("Failed to fetch from server: $failure") - LoadMoreResult.FAILURE - } + // Fetch prev chunk from server if not in the db + val token = chunkEntity.prevToken + fetchFromServer(token, direction) } else { // otherwise we delegate to the prev chunk if (prevChunk == null) { @@ -193,10 +185,26 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, ) } - private suspend fun fetchFromServer(token: String, direction: Timeline.Direction): LoadMoreResult { - val paginationParams = PaginationTask.Params(roomId, token, direction.toPaginationDirection(), PAGINATION_COUNT) - val paginationResult = paginationTask.execute(paginationParams) - return when (paginationResult) { + private suspend fun fetchFromServer(token: String?, direction: Timeline.Direction): LoadMoreResult { + val paginationResult = try { + if (token == null) { + if (direction == Timeline.Direction.BACKWARDS || !chunkEntity.hasBeenALastForwardChunk()) return LoadMoreResult.REACHED_END + val lastKnownEventId = chunkEntity.sortedTimelineEvents().firstOrNull()?.eventId ?: return LoadMoreResult.FAILURE + val taskParams = FetchTokenAndPaginateTask.Params(roomId, lastKnownEventId, direction.toPaginationDirection(), PAGINATION_COUNT) + fetchTokenAndPaginateTask.execute(taskParams) + } else { + val taskParams = PaginationTask.Params(roomId, token, direction.toPaginationDirection(), PAGINATION_COUNT) + paginationTask.execute(taskParams) + } + } catch (failure: Throwable) { + Timber.e("Failed to fetch from server: $failure", failure) + return LoadMoreResult.FAILURE + } + return paginationResult.toLoadMoreResult() + } + + private fun TokenChunkEventPersistor.Result.toLoadMoreResult(): LoadMoreResult { + return when (this) { TokenChunkEventPersistor.Result.REACHED_END -> LoadMoreResult.REACHED_END TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE, TokenChunkEventPersistor.Result.SUCCESS -> LoadMoreResult.SUCCESS From 7f9c1916477d63c38f9bf8b0c19aebeefa27757d Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 17 Sep 2021 18:01:26 +0200 Subject: [PATCH 06/29] Timeline rework: add db migration --- .../database/RealmSessionStoreMigration.kt | 24 ++++++++++++++++++- .../SessionRealmConfigurationFactory.kt | 1 - 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index aa96ca5e1a..e1d27647a6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent import org.matrix.android.sdk.api.session.room.model.tag.RoomTag +import org.matrix.android.sdk.internal.database.model.ChunkEntityFields import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields import org.matrix.android.sdk.internal.database.model.EditionOfEventFields @@ -47,7 +48,7 @@ import timber.log.Timber internal object RealmSessionStoreMigration : RealmMigration { - const val SESSION_STORE_SCHEMA_VERSION = 17L + const val SESSION_STORE_SCHEMA_VERSION = 18L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.v("Migrating Realm Session from $oldVersion to $newVersion") @@ -69,6 +70,7 @@ internal object RealmSessionStoreMigration : RealmMigration { if (oldVersion <= 14) migrateTo15(realm) if (oldVersion <= 15) migrateTo16(realm) if (oldVersion <= 16) migrateTo17(realm) + if (oldVersion <= 17) migrateTo18(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -338,4 +340,24 @@ internal object RealmSessionStoreMigration : RealmMigration { realm.schema.get("EventInsertEntity") ?.addField(EventInsertEntityFields.CAN_BE_PROCESSED, Boolean::class.java) } + + private fun migrateTo18(realm: DynamicRealm) { + Timber.d("Step 17 -> 18") + realm.schema.get("ChunkEntity")?.apply { + removeField("numberOfTimelineEvents") + var cleanOldChunks = false + if (!hasField(ChunkEntityFields.NEXT_CHUNK.`$`)) { + cleanOldChunks = true + addRealmObjectField(ChunkEntityFields.NEXT_CHUNK.`$`, this) + } + if (!hasField(ChunkEntityFields.PREV_CHUNK.`$`)) { + cleanOldChunks = true + addRealmObjectField(ChunkEntityFields.PREV_CHUNK.`$`, this) + } + if (cleanOldChunks) { + val chunkEntities = realm.where("ChunkEntity").equalTo(ChunkEntityFields.IS_LAST_FORWARD, false).findAll() + chunkEntities.deleteAllFromRealm() + } + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt index 6aeb3936a9..1771c5b202 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt @@ -72,7 +72,6 @@ internal class SessionRealmConfigurationFactory @Inject constructor( .modules(SessionRealmModule()) .schemaVersion(RealmSessionStoreMigration.SESSION_STORE_SCHEMA_VERSION) .migration(RealmSessionStoreMigration) - .deleteRealmIfMigrationNeeded() .build() // Try creating a realm instance and if it succeeds we can clear the flag From b370f84e08feafb0391556f0e70b15f72ddfe089 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 20 Sep 2021 18:33:26 +0200 Subject: [PATCH 07/29] Timeline rework: add some comments and fix pagination when having overlapping events --- .../room/timeline/LoadTimelineStrategy.kt | 7 + .../session/room/timeline/TimelineChunk.kt | 161 +++++++++--------- .../room/timeline/TokenChunkEventPersistor.kt | 124 ++++---------- 3 files changed, 121 insertions(+), 171 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index 0c8a1ae903..3d8eb89315 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -31,6 +31,13 @@ import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents import org.matrix.android.sdk.internal.database.query.where import java.util.concurrent.atomic.AtomicReference +/** + * This class is responsible for keeping an instance of chunkEntity and timelineChunk according to the strategy. + * There is 2 different mode: Default and Permalink. + * In Default, we will query for the live chunk (isLastForward = true). + * In Permalink, we will query for the chunk including the eventId we are looking for. + * Once we got a ChunkEntity we wrap it with TimelineChunk class so we dispatch any methods for loading data. + */ internal class LoadTimelineStrategy( private val roomId: String, private val timelineId: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index 45c74fa446..3e46681358 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -38,6 +38,11 @@ import java.util.Collections */ private const val PAGINATION_COUNT = 50 +/** + * This is a wrapper around a ChunkEntity in the database. + * It does mainly listen to the db timeline events. + * It also triggers pagination to the server when needed, or dispatch to the prev or next chunk if any. + */ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, private val roomId: String, private val timelineId: String, @@ -56,7 +61,7 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, private val timelineEventCollectionListener = OrderedRealmCollectionChangeListener { results: RealmResults, changeSet: OrderedCollectionChangeSet -> val frozenResults = results.freeze() Timber.v("on timeline event changed: $changeSet") - handleChangeSet(frozenResults, changeSet) + handleDatabaseChangeSet(frozenResults, changeSet) } private var timelineEventEntities: RealmResults = chunkEntity.sortedTimelineEvents() @@ -130,6 +135,82 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, } } + fun getBuiltEventIndex(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): Int? { + val builtEventIndex = builtEventsIndexes[eventId] + if (builtEventIndex != null) { + return getOffsetIndex() + builtEventIndex + } + if (searchInNext) { + val nextBuiltEventIndex = nextChunk?.getBuiltEventIndex(eventId, searchInNext = true, searchInPrev = false) + if (nextBuiltEventIndex != null) { + return nextBuiltEventIndex + } + } + if (searchInPrev) { + val prevBuiltEventIndex = prevChunk?.getBuiltEventIndex(eventId, searchInNext = false, searchInPrev = true) + if (prevBuiltEventIndex != null) { + return prevBuiltEventIndex + } + } + return null + } + + fun getBuiltEvent(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): TimelineEvent? { + val builtEventIndex = builtEventsIndexes[eventId] + if (builtEventIndex != null) { + return builtEvents.getOrNull(builtEventIndex) + } + if (searchInNext) { + val nextBuiltEvent = nextChunk?.getBuiltEvent(eventId, searchInNext = true, searchInPrev = false) + if (nextBuiltEvent != null) { + return nextBuiltEvent + } + } + if (searchInPrev) { + val prevBuiltEvent = prevChunk?.getBuiltEvent(eventId, searchInNext = false, searchInPrev = true) + if (prevBuiltEvent != null) { + return prevBuiltEvent + } + } + return null + } + + fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?, searchInNext: Boolean, searchInPrev: Boolean): Boolean { + return tryOrNull { + val builtIndex = getBuiltEventIndex(eventId, searchInNext = false, searchInPrev = false) + if (builtIndex == null) { + val foundInPrev = searchInPrev && prevChunk?.rebuildEvent(eventId, builder, searchInNext = false, searchInPrev = true).orFalse() + if (foundInPrev) { + return true + } + if (searchInNext) { + return prevChunk?.rebuildEvent(eventId, builder, searchInPrev = false, searchInNext = true).orFalse() + } + return false + } + // Update the relation of existing event + builtEvents.getOrNull(builtIndex)?.let { te -> + val rebuiltEvent = builder(te) + builtEvents[builtIndex] = rebuiltEvent!! + true + } + } + ?: false + } + + fun close(closeNext: Boolean, closePrev: Boolean) { + if (closeNext) { + nextChunk?.close(closeNext = true, closePrev = false) + } + if (closePrev) { + prevChunk?.close(closeNext = false, closePrev = true) + } + nextChunk = null + prevChunk = null + chunkEntity.removeChangeListener(chunkObjectListener) + timelineEventEntities.removeChangeListener(timelineEventCollectionListener) + } + private fun loadFromDb(count: Long, direction: Timeline.Direction): Long { val displayIndex = getNextDisplayIndex(direction) ?: return 0 val baseQuery = timelineEventEntities.where() @@ -211,46 +292,6 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, } } - fun getBuiltEventIndex(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): Int? { - val builtEventIndex = builtEventsIndexes[eventId] - if (builtEventIndex != null) { - return getOffsetIndex() + builtEventIndex - } - if (searchInNext) { - val nextBuiltEventIndex = nextChunk?.getBuiltEventIndex(eventId, searchInNext = true, searchInPrev = false) - if (nextBuiltEventIndex != null) { - return nextBuiltEventIndex - } - } - if (searchInPrev) { - val prevBuiltEventIndex = prevChunk?.getBuiltEventIndex(eventId, searchInNext = false, searchInPrev = true) - if (prevBuiltEventIndex != null) { - return prevBuiltEventIndex - } - } - return null - } - - fun getBuiltEvent(eventId: String, searchInNext: Boolean, searchInPrev: Boolean): TimelineEvent? { - val builtEventIndex = builtEventsIndexes[eventId] - if (builtEventIndex != null) { - return builtEvents.getOrNull(builtEventIndex) - } - if (searchInNext) { - val nextBuiltEvent = nextChunk?.getBuiltEvent(eventId, searchInNext = true, searchInPrev = false) - if (nextBuiltEvent != null) { - return nextBuiltEvent - } - } - if (searchInPrev) { - val prevBuiltEvent = prevChunk?.getBuiltEvent(eventId, searchInNext = false, searchInPrev = true) - if (prevBuiltEvent != null) { - return prevBuiltEvent - } - } - return null - } - private fun getOffsetIndex(): Int { var offset = 0 var currentNextChunk = nextChunk @@ -261,7 +302,7 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, return offset } - private fun handleChangeSet(frozenResults: RealmResults, changeSet: OrderedCollectionChangeSet) { + private fun handleDatabaseChangeSet(frozenResults: RealmResults, changeSet: OrderedCollectionChangeSet) { val insertions = changeSet.insertionRanges for (range in insertions) { val newItems = frozenResults @@ -290,42 +331,6 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, } } - fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent?, searchInNext: Boolean, searchInPrev: Boolean): Boolean { - return tryOrNull { - val builtIndex = getBuiltEventIndex(eventId, searchInNext = false, searchInPrev = false) - if (builtIndex == null) { - val foundInPrev = searchInPrev && prevChunk?.rebuildEvent(eventId, builder, searchInNext = false, searchInPrev = true).orFalse() - if (foundInPrev) { - return true - } - if (searchInNext) { - return prevChunk?.rebuildEvent(eventId, builder, searchInPrev = false, searchInNext = true).orFalse() - } - return false - } - // Update the relation of existing event - builtEvents.getOrNull(builtIndex)?.let { te -> - val rebuiltEvent = builder(te) - builtEvents[builtIndex] = rebuiltEvent!! - true - } - } - ?: false - } - - fun close(closeNext: Boolean, closePrev: Boolean) { - if (closeNext) { - nextChunk?.close(closeNext = true, closePrev = false) - } - if (closePrev) { - prevChunk?.close(closeNext = false, closePrev = true) - } - nextChunk = null - prevChunk = null - chunkEntity.removeChangeListener(chunkObjectListener) - timelineEventEntities.removeChangeListener(timelineEventCollectionListener) - } - private fun getNextDisplayIndex(direction: Timeline.Direction): Int? { val frozenTimelineEvents = timelineEventEntities.freeze() if (frozenTimelineEvents.isEmpty()) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt index 13704a5168..43c2c37f84 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -40,73 +40,10 @@ import timber.log.Timber import javax.inject.Inject /** - * Insert Chunk in DB, and eventually merge with existing chunk event + * Insert Chunk in DB, and eventually link next and previous chunk in db. */ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { - /** - *
-     * ========================================================================================================
-     * | Backward case                                                                                        |
-     * ========================================================================================================
-     *
-     *                               *--------------------------*        *--------------------------*
-     *                               | startToken1              |        | startToken1              |
-     *                               *--------------------------*        *--------------------------*
-     *                               |                          |        |                          |
-     *                               |                          |        |                          |
-     *                               |  receivedChunk backward  |        |                          |
-     *                               |         Events           |        |                          |
-     *                               |                          |        |                          |
-     *                               |                          |        |                          |
-     *                               |                          |        |                          |
-     * *--------------------------*  *--------------------------*        |                          |
-     * | startToken0              |  | endToken1                |   =>   |       Merged chunk       |
-     * *--------------------------*  *--------------------------*        |          Events          |
-     * |                          |                                      |                          |
-     * |                          |                                      |                          |
-     * |      Current Chunk       |                                      |                          |
-     * |         Events           |                                      |                          |
-     * |                          |                                      |                          |
-     * |                          |                                      |                          |
-     * |                          |                                      |                          |
-     * *--------------------------*                                      *--------------------------*
-     * | endToken0                |                                      | endToken0                |
-     * *--------------------------*                                      *--------------------------*
-     *
-     *
-     * ========================================================================================================
-     * | Forward case                                                                                         |
-     * ========================================================================================================
-     *
-     * *--------------------------*                                      *--------------------------*
-     * | startToken0              |                                      | startToken0              |
-     * *--------------------------*                                      *--------------------------*
-     * |                          |                                      |                          |
-     * |                          |                                      |                          |
-     * |      Current Chunk       |                                      |                          |
-     * |         Events           |                                      |                          |
-     * |                          |                                      |                          |
-     * |                          |                                      |                          |
-     * |                          |                                      |                          |
-     * *--------------------------*  *--------------------------*        |                          |
-     * | endToken0                |  | startToken1              |   =>   |       Merged chunk       |
-     * *--------------------------*  *--------------------------*        |          Events          |
-     *                               |                          |        |                          |
-     *                               |                          |        |                          |
-     *                               |  receivedChunk forward   |        |                          |
-     *                               |         Events           |        |                          |
-     *                               |                          |        |                          |
-     *                               |                          |        |                          |
-     *                               |                          |        |                          |
-     *                               *--------------------------*        *--------------------------*
-     *                               | endToken1                |        | endToken1                |
-     *                               *--------------------------*        *--------------------------*
-     *
-     * ========================================================================================================
-     * 
- */ - enum class Result { SHOULD_FETCH_MORE, REACHED_END, @@ -191,38 +128,39 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri roomMemberContentsByUser[stateEvent.stateKey] = stateEvent.content.toModel() } } - val eventIds = ArrayList(eventList.size) - eventList.forEach { event -> - if (event.eventId == null || event.senderId == null) { - return@forEach - } - //We check for the timeline event with this id - val eventId = event.eventId - val existingTimelineEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst() - // If it exists, we want to skip here - val existingChunk = existingTimelineEvent?.chunk?.firstOrNull() - if (existingChunk != null) { - if (direction == PaginationDirection.BACKWARDS) { - currentChunk.prevChunk = existingChunk - existingChunk.nextChunk = currentChunk - } else if (direction == PaginationDirection.FORWARDS) { - currentChunk.nextChunk = existingChunk - existingChunk.prevChunk = currentChunk + run processTimelineEvents@ { + eventList.forEach { event -> + if (event.eventId == null || event.senderId == null) { + return@forEach } - return@forEach - } - val ageLocalTs = event.unsignedData?.age?.let { now - it } - eventIds.add(event.eventId) - val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) - if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) { - val contentToUse = if (direction == PaginationDirection.BACKWARDS) { - event.prevContent - } else { - event.content + //We check for the timeline event with this id + val eventId = event.eventId + val existingTimelineEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst() + // If it exists, we want to stop here, just link the prevChunk + val existingChunk = existingTimelineEvent?.chunk?.firstOrNull() + if (existingChunk != null) { + if (direction == PaginationDirection.BACKWARDS) { + currentChunk.prevChunk = existingChunk + existingChunk.nextChunk = currentChunk + } else if (direction == PaginationDirection.FORWARDS) { + currentChunk.nextChunk = existingChunk + existingChunk.prevChunk = currentChunk + } + // Stop processing here + return@processTimelineEvents } - roomMemberContentsByUser[event.stateKey] = contentToUse.toModel() + val ageLocalTs = event.unsignedData?.age?.let { now - it } + val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) + if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) { + val contentToUse = if (direction == PaginationDirection.BACKWARDS) { + event.prevContent + } else { + event.content + } + roomMemberContentsByUser[event.stateKey] = contentToUse.toModel() + } + currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser) } - currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser) } if (currentChunk.isValid) { RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk) From cd1da7348f795dcb7637c57a3b1b30b16c8e1231 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 20 Sep 2021 18:33:43 +0200 Subject: [PATCH 08/29] Timeline rework: make sure migration doesn't crash --- .../sdk/internal/database/RealmSessionStoreMigration.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index e1d27647a6..5aa6914647 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -344,7 +344,9 @@ internal object RealmSessionStoreMigration : RealmMigration { private fun migrateTo18(realm: DynamicRealm) { Timber.d("Step 17 -> 18") realm.schema.get("ChunkEntity")?.apply { - removeField("numberOfTimelineEvents") + if (hasField("numberOfTimelineEvents")) { + removeField("numberOfTimelineEvents") + } var cleanOldChunks = false if (!hasField(ChunkEntityFields.NEXT_CHUNK.`$`)) { cleanOldChunks = true From d42a2e69aeb5161fdc770f18144d22c7f71547f8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 3 Nov 2021 11:44:01 +0100 Subject: [PATCH 09/29] Timeline: don't remove annotations and read receipts when deleting timeline event --- .../android/sdk/internal/database/model/TimelineEventEntity.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt index 30bbde70c2..185f0e2dcc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt @@ -46,7 +46,5 @@ internal fun TimelineEventEntity.deleteOnCascade(canDeleteRoot: Boolean) { if (canDeleteRoot) { root?.deleteFromRealm() } - annotations?.deleteOnCascade() - readReceipts?.deleteOnCascade() deleteFromRealm() } From ce5ccd4dab5a18364526fcf7559b84f6473c0cb1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 3 Nov 2021 11:44:20 +0100 Subject: [PATCH 10/29] Timeline: remove useless methods --- .../sdk/api/session/room/timeline/Timeline.kt | 20 ------------------- .../session/room/timeline/DefaultTimeline.kt | 19 +----------------- .../detail/timeline/merged/MergedTimelines.kt | 18 ----------------- 3 files changed, 1 insertion(+), 56 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt index ae202da979..3ba2aadd38 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt @@ -70,31 +70,11 @@ interface Timeline { */ fun paginate(direction: Direction, count: Int) - /** - * Returns the number of sending events - */ - fun pendingEventCount(): Int - - /** - * Returns the number of failed sending events. - */ - fun failedToDeliverEventCount(): Int - /** * Returns the index of a built event or null. */ fun getIndexOfEvent(eventId: String?): Int? - /** - * Returns the built [TimelineEvent] at index or null - */ - fun getTimelineEventAtIndex(index: Int): TimelineEvent? - - /** - * Returns the built [TimelineEvent] with eventId or null - */ - fun getTimelineEventWithId(eventId: String?): TimelineEvent? - fun getPaginationState(direction: Direction): PaginationState interface Listener { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index b4e839ae21..857075ffde 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -54,7 +54,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, eventDecryptor: TimelineEventDecryptor) : Timeline { companion object { - val BACKGROUND_HANDLER = createBackgroundHandler("SimpleTimeline_Thread") + val BACKGROUND_HANDLER = createBackgroundHandler("DefaultTimeline_Thread") } override val timelineID = UUID.randomUUID().toString() @@ -144,28 +144,11 @@ internal class DefaultTimeline internal constructor(private val roomId: String, } } - override fun pendingEventCount(): Int { - return 0 - } - - override fun failedToDeliverEventCount(): Int { - return 0 - } - override fun getIndexOfEvent(eventId: String?): Int? { if (eventId == null) return null return strategy.getBuiltEventIndex(eventId) } - override fun getTimelineEventAtIndex(index: Int): TimelineEvent? { - return null - } - - override fun getTimelineEventWithId(eventId: String?): TimelineEvent? { - if (eventId == null) return null - return strategy.getBuiltEvent(eventId) - } - override fun getPaginationState(direction: Timeline.Direction): Timeline.PaginationState { return if (direction == Timeline.Direction.BACKWARDS) { backwardState diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/merged/MergedTimelines.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/merged/MergedTimelines.kt index 0d5dbc5a8e..535b733dc2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/merged/MergedTimelines.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/merged/MergedTimelines.kt @@ -133,28 +133,10 @@ class MergedTimelines( secondaryTimeline.paginate(direction, count) } - override fun pendingEventCount(): Int { - return mainTimeline.pendingEventCount() + secondaryTimeline.pendingEventCount() - } - - override fun failedToDeliverEventCount(): Int { - return mainTimeline.pendingEventCount() + secondaryTimeline.pendingEventCount() - } - - override fun getTimelineEventAtIndex(index: Int): TimelineEvent? { - return mergedEvents.getOrNull(index) - } - override fun getIndexOfEvent(eventId: String?): Int? { return positionsMapping[eventId] } - override fun getTimelineEventWithId(eventId: String?): TimelineEvent? { - return positionsMapping[eventId]?.let { - getTimelineEventAtIndex(it) - } - } - private fun processTimelineUpdates(isInit: KMutableProperty0, eventsRef: MutableList, newData: List) { coroutineScope.launch(Dispatchers.Default) { processingSemaphore.withPermit { From 52d0da705316328499bc33abd2b3c6f917faf45c Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 3 Nov 2021 19:02:44 +0100 Subject: [PATCH 11/29] Timeline: remove previous lastForward chunk --- .../session/sync/handler/room/RoomSyncHandler.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 8c4af81c99..4a3a3617ca 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -344,15 +344,17 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle syncLocalTimestampMillis: Long, aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity { val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId) + if (isLimited && lastChunk != null) { + lastChunk.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true) + } val chunkEntity = if (!isLimited && lastChunk != null) { lastChunk } else { - realm.createObject().apply { this.prevToken = prevToken } + realm.createObject().apply { + this.prevToken = prevToken + this.isLastForward = true + } } - // Only one chunk has isLastForward set to true - lastChunk?.isLastForward = false - chunkEntity.isLastForward = true - val eventIds = ArrayList(eventList.size) val roomMemberContentsByUser = HashMap() From a1fdd31b6800f570999f286ed379afa26d837faa Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 3 Nov 2021 19:02:58 +0100 Subject: [PATCH 12/29] Timeline: just some renaming + constant --- .../session/room/timeline/DefaultTimeline.kt | 5 ++--- .../session/room/timeline/LoadTimelineStrategy.kt | 15 +++++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 857075ffde..e4173e90f8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -29,7 +29,6 @@ import kotlinx.coroutines.withContext import okhttp3.internal.closeQuietly import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.room.timeline.Timeline -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler @@ -81,7 +80,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, onNewTimelineEvents = this::onNewTimelineEvents ) - private var strategy: LoadTimelineStrategy = buildStrategy(LoadTimelineStrategy.Mode.Default) + private var strategy: LoadTimelineStrategy = buildStrategy(LoadTimelineStrategy.Mode.Live) override val isLive: Boolean get() = !getPaginationState(Timeline.Direction.FORWARDS).hasMoreToLoad @@ -191,7 +190,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, } strategy.onStop() strategy = if (eventId == null) { - buildStrategy(LoadTimelineStrategy.Mode.Default) + buildStrategy(LoadTimelineStrategy.Mode.Live) } else { buildStrategy(LoadTimelineStrategy.Mode.Permalink(eventId)) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index 3d8eb89315..975ef4e56d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -33,11 +33,14 @@ import java.util.concurrent.atomic.AtomicReference /** * This class is responsible for keeping an instance of chunkEntity and timelineChunk according to the strategy. - * There is 2 different mode: Default and Permalink. - * In Default, we will query for the live chunk (isLastForward = true). + * There is 2 different mode: Live and Permalink. + * In Live, we will query for the live chunk (isLastForward = true). * In Permalink, we will query for the chunk including the eventId we are looking for. * Once we got a ChunkEntity we wrap it with TimelineChunk class so we dispatch any methods for loading data. */ + +private const val INITIAL_LOAD_COUNT = 30L + internal class LoadTimelineStrategy( private val roomId: String, private val timelineId: String, @@ -45,7 +48,7 @@ internal class LoadTimelineStrategy( private val dependencies: Dependencies) { sealed class Mode { - object Default : Mode() + object Live : Mode() data class Permalink(val originEventId: String) : Mode() fun originEventId(): String? { @@ -109,7 +112,7 @@ internal class LoadTimelineStrategy( override fun onNewTimelineEvents(roomId: String, eventIds: List) { super.onNewTimelineEvents(roomId, eventIds) - if (mode == Mode.Default && roomId == this@LoadTimelineStrategy.roomId) { + if (mode == Mode.Live && roomId == this@LoadTimelineStrategy.roomId) { dependencies.onNewTimelineEvents(eventIds) } } @@ -133,8 +136,8 @@ internal class LoadTimelineStrategy( it.addChangeListener(chunkEntityListener) timelineChunk = it.createTimelineChunk() } - if (mode is Mode.Default) { - loadMore(10, Timeline.Direction.BACKWARDS) + if (mode is Mode.Live) { + loadMore(INITIAL_LOAD_COUNT, Timeline.Direction.BACKWARDS) } } From 92a37f15d4b4def2de216049552181a1374da9b8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 4 Nov 2021 13:11:45 +0100 Subject: [PATCH 13/29] Timeline: fix hasReachedEnd --- .../session/room/timeline/LoadTimelineStrategy.kt | 9 ++++++--- .../sdk/internal/session/room/timeline/TimelineChunk.kt | 9 ++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index 975ef4e56d..3f4d751f6d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -111,8 +111,7 @@ internal class LoadTimelineStrategy( } override fun onNewTimelineEvents(roomId: String, eventIds: List) { - super.onNewTimelineEvents(roomId, eventIds) - if (mode == Mode.Live && roomId == this@LoadTimelineStrategy.roomId) { + if (roomId == this@LoadTimelineStrategy.roomId && hasReachedLastForward()) { dependencies.onNewTimelineEvents(eventIds) } } @@ -178,7 +177,7 @@ internal class LoadTimelineStrategy( } private fun buildSendingEvents(): List { - return if (timelineChunk?.hasReachedLastForward().orFalse()) { + return if (hasReachedLastForward()) { sendingEventsDataSource.buildSendingEvents() } else { emptyList() @@ -195,6 +194,10 @@ internal class LoadTimelineStrategy( } } + private fun hasReachedLastForward(): Boolean{ + return timelineChunk?.hasReachedLastForward().orFalse() + } + private fun RealmResults.createTimelineChunk(): TimelineChunk? { return firstOrNull()?.let { return TimelineChunk( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index 3e46681358..fcea10250b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -28,10 +28,12 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.model.ChunkEntity +import org.matrix.android.sdk.internal.database.model.ChunkEntityFields import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import timber.log.Timber import java.util.Collections +import java.util.concurrent.atomic.AtomicBoolean /** * This is the value used to fetch on server. It's better to make constant as otherwise we can have weird chunks with disparate and small chunk of data. @@ -54,8 +56,13 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, private val initialEventId: String?, private val onBuiltEvents: () -> Unit) { + private val isLastForward = AtomicBoolean(chunkEntity.isLastForward) + private val chunkObjectListener = RealmObjectChangeListener { _, changeSet -> Timber.v("on chunk (${chunkEntity.identifier()}) changed: ${changeSet?.changedFields?.joinToString(",")}") + if(changeSet?.isFieldChanged(ChunkEntityFields.IS_LAST_FORWARD).orFalse()){ + isLastForward.set(chunkEntity.isLastForward) + } } private val timelineEventCollectionListener = OrderedRealmCollectionChangeListener { results: RealmResults, changeSet: OrderedCollectionChangeSet -> @@ -77,7 +84,7 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, } fun hasReachedLastForward(): Boolean { - return if (chunkEntity.isLastForward) { + return if (isLastForward.get()) { true } else { nextChunk?.hasReachedLastForward().orFalse() From 52df50a6865289421e56e6c6a6c768ce0e50bba5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 10 Nov 2021 19:17:34 +0100 Subject: [PATCH 14/29] Timeline: continue trying to make Read marker/receipts working --- .../session/room/timeline/TimelineEvent.kt | 4 +++ .../database/helper/ChunkEntityHelper.kt | 26 +++++++++++++++++++ .../helper/TimelineEventEntityHelper.kt | 10 +++++++ .../internal/database/query/ReadQueries.kt | 24 ++++++++--------- .../session/room/read/DefaultReadService.kt | 1 - .../home/room/detail/RoomDetailViewModel.kt | 11 +++++--- 6 files changed, 60 insertions(+), 16 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 4a6462477d..b9c7c812ae 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -44,6 +44,10 @@ data class TimelineEvent( */ val localId: Long, val eventId: String, + /** + * This display index is the position in the current chunk. + * It's not unique on the timeline as it's reset on each chunk. + */ val displayIndex: Int, val senderInfo: SenderInfo, val annotations: EventAnnotationsSummary? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt index 482f4ad842..136734c02e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt @@ -191,3 +191,29 @@ internal fun ChunkEntity.nextDisplayIndex(direction: PaginationDirection): Int { } } } + +internal fun ChunkEntity.doesNextChunksVerifyCondition(linkCondition: (ChunkEntity) -> Boolean): Boolean { + var nextChunkToCheck = this.nextChunk + while (nextChunkToCheck != null) { + if (linkCondition(nextChunkToCheck)) { + return true + } + nextChunkToCheck = nextChunkToCheck.nextChunk + } + return false +} + +internal fun ChunkEntity.isMoreRecentThan(chunkToCheck: ChunkEntity): Boolean { + if (this.isLastForward) return true + if (chunkToCheck.isLastForward) return false + // Check if the chunk to check is linked to this one + if(chunkToCheck.doesNextChunksVerifyCondition { it == this }){ + return true + } + // Otherwise check if this chunk is linked to last forward + if(this.doesNextChunksVerifyCondition { it.isLastForward }){ + return true + } + // We don't know, so we assume it's false + return false +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt index 3993e8e799..1d2cbcad51 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/TimelineEventEntityHelper.kt @@ -28,3 +28,13 @@ internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long { currentIdNum.toLong() + 1 } } + +internal fun TimelineEventEntity.isMoreRecentThan(eventToCheck: TimelineEventEntity): Boolean { + val currentChunk = this.chunk?.first() ?: return false + val chunkToCheck = eventToCheck.chunk?.firstOrNull() ?: return false + return if (currentChunk == chunkToCheck) { + this.displayIndex >= eventToCheck.displayIndex + } else { + currentChunk.isMoreRecentThan(chunkToCheck) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt index 60096777d9..c8879b009e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt @@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.database.query import io.realm.Realm import io.realm.RealmConfiguration import org.matrix.android.sdk.api.session.events.model.LocalEcho +import org.matrix.android.sdk.internal.database.helper.doesNextChunksVerifyCondition +import org.matrix.android.sdk.internal.database.helper.isMoreRecentThan import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity @@ -33,28 +35,26 @@ internal fun isEventRead(realmConfiguration: RealmConfiguration, if (LocalEcho.isLocalEchoId(eventId)) { return true } + // If we don't know if the event has been read, we assume it's not var isEventRead = false Realm.getInstance(realmConfiguration).use { realm -> - val liveChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: return@use - val eventToCheck = liveChunk.timelineEvents.find(eventId) + val latestEvent = TimelineEventEntity.latestEvent(realm, roomId, true) + // If latest event is from you we are sure the event is read + if (latestEvent?.root?.sender == userId) { + return true + } + val eventToCheck = TimelineEventEntity.where(realm, roomId, eventId).findFirst() isEventRead = when { - eventToCheck == null -> hasReadMissingEvent( - realm = realm, - latestChunkEntity = liveChunk, - roomId = roomId, - userId = userId, - eventId = eventId - ) + eventToCheck == null -> false eventToCheck.root?.sender == userId -> true else -> { val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@use - val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.displayIndex ?: Int.MIN_VALUE - eventToCheck.displayIndex <= readReceiptIndex + val readReceiptEvent = TimelineEventEntity.where(realm, roomId, readReceipt.eventId).findFirst() ?: return@use + readReceiptEvent.isMoreRecentThan(eventToCheck) } } } - return isEventRead } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt index 28f55a01ee..7e42e4f9c2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt @@ -39,7 +39,6 @@ import org.matrix.android.sdk.internal.task.TaskExecutor internal class DefaultReadService @AssistedInject constructor( @Assisted private val roomId: String, @SessionDatabase private val monarchy: Monarchy, - private val taskExecutor: TaskExecutor, private val setReadMarkersTask: SetReadMarkersTask, private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, @UserId private val userId: String diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 03bde7d4cc..4918618683 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -794,7 +794,6 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) { - stopTrackingUnreadMessages() val targetEventId: String = action.eventId val indexOfEvent = timeline.getIndexOfEvent(targetEventId) if (indexOfEvent == null) { @@ -868,12 +867,12 @@ class RoomDetailViewModel @AssistedInject constructor( .buffer(1, TimeUnit.SECONDS) .filter { it.isNotEmpty() } .subscribeBy(onNext = { actions -> - val bufferedMostRecentDisplayedEvent = actions.maxByOrNull { it.event.displayIndex }?.event ?: return@subscribeBy + val bufferedMostRecentDisplayedEvent = actions.minByOrNull { it.event.indexOfEvent() }?.event ?: return@subscribeBy val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent if (trackUnreadMessages.get()) { if (globalMostRecentDisplayedEvent == null) { mostRecentDisplayedEvent = bufferedMostRecentDisplayedEvent - } else if (bufferedMostRecentDisplayedEvent.displayIndex > globalMostRecentDisplayedEvent.displayIndex) { + } else if (bufferedMostRecentDisplayedEvent.indexOfEvent() < globalMostRecentDisplayedEvent.indexOfEvent()) { mostRecentDisplayedEvent = bufferedMostRecentDisplayedEvent } } @@ -886,6 +885,12 @@ class RoomDetailViewModel @AssistedInject constructor( .disposeOnClear() } + /** + * Returns the index of event in the timeline. + * Returns Int.MAX_VALUE if not found + */ + private fun TimelineEvent.indexOfEvent(): Int = timeline.getIndexOfEvent(eventId) ?: Int.MAX_VALUE + private fun handleMarkAllAsRead() { setState { copy(unreadState = UnreadState.HasNoUnread) } viewModelScope.launch { From 8c0b2a6704da7b74e7edc9f299e5388748ed7a04 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 16 Nov 2021 18:14:11 +0100 Subject: [PATCH 15/29] Timeline: fix double link issue when server is messed up... --- .../room/timeline/TokenChunkEventPersistor.kt | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt index 43c2c37f84..54d7412166 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -128,7 +128,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri roomMemberContentsByUser[stateEvent.stateKey] = stateEvent.content.toModel() } } - run processTimelineEvents@ { + run processTimelineEvents@{ eventList.forEach { event -> if (event.eventId == null || event.senderId == null) { return@forEach @@ -139,12 +139,23 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri // If it exists, we want to stop here, just link the prevChunk val existingChunk = existingTimelineEvent?.chunk?.firstOrNull() if (existingChunk != null) { - if (direction == PaginationDirection.BACKWARDS) { - currentChunk.prevChunk = existingChunk - existingChunk.nextChunk = currentChunk - } else if (direction == PaginationDirection.FORWARDS) { - currentChunk.nextChunk = existingChunk - existingChunk.prevChunk = currentChunk + when (direction) { + PaginationDirection.BACKWARDS -> { + if (currentChunk.nextChunk == existingChunk) { + Timber.w("Avoid double link, shouldn't happen in an ideal world") + } else { + currentChunk.prevChunk = existingChunk + existingChunk.nextChunk = currentChunk + } + } + PaginationDirection.FORWARDS -> { + if (currentChunk.prevChunk == existingChunk) { + Timber.w("Avoid double link, shouldn't happen in an ideal world") + } else { + currentChunk.nextChunk = existingChunk + existingChunk.prevChunk = currentChunk + } + } } // Stop processing here return@processTimelineEvents From e562d7684ad25b3286f35c79a8d1ec31cf42a1b4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 18 Nov 2021 11:03:13 +0100 Subject: [PATCH 16/29] Timeline: re-add usage of TimelineSettings --- .../internal/session/room/timeline/DefaultTimeline.kt | 3 +++ .../session/room/timeline/DefaultTimelineService.kt | 1 + .../session/room/timeline/LoadTimelineStrategy.kt | 7 ++++--- .../sdk/internal/session/room/timeline/TimelineChunk.kt | 9 ++++++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index e4173e90f8..82684f8c7e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.withContext import okhttp3.internal.closeQuietly import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler @@ -42,6 +43,7 @@ import java.util.concurrent.atomic.AtomicReference internal class DefaultTimeline internal constructor(private val roomId: String, private val initialEventId: String?, + private val settings: TimelineSettings, private val realmConfiguration: RealmConfiguration, private val loadRoomMembersTask: LoadRoomMembersTask, private val readReceiptHandler: ReadReceiptHandler, @@ -70,6 +72,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, private val strategyDependencies = LoadTimelineStrategy.Dependencies( eventDecryptor = eventDecryptor, + timelineSettings = settings, paginationTask = paginationTask, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, timelineInput = timelineInput, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index a898510654..31a6e627ec 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -64,6 +64,7 @@ internal class DefaultTimelineService @AssistedInject constructor( return DefaultTimeline( roomId = roomId, initialEventId = eventId, + settings = settings, realmConfiguration = monarchy.realmConfiguration, paginationTask = paginationTask, timelineEventMapper = timelineEventMapper, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index 3f4d751f6d..72613e5c6b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ChunkEntityFields @@ -39,8 +40,6 @@ import java.util.concurrent.atomic.AtomicReference * Once we got a ChunkEntity we wrap it with TimelineChunk class so we dispatch any methods for loading data. */ -private const val INITIAL_LOAD_COUNT = 30L - internal class LoadTimelineStrategy( private val roomId: String, private val timelineId: String, @@ -61,6 +60,7 @@ internal class LoadTimelineStrategy( } data class Dependencies( + val timelineSettings: TimelineSettings, val realm: AtomicReference, val eventDecryptor: TimelineEventDecryptor, val paginationTask: PaginationTask, @@ -136,7 +136,7 @@ internal class LoadTimelineStrategy( timelineChunk = it.createTimelineChunk() } if (mode is Mode.Live) { - loadMore(INITIAL_LOAD_COUNT, Timeline.Direction.BACKWARDS) + loadMore(dependencies.timelineSettings.initialSize.toLong(), Timeline.Direction.BACKWARDS) } } @@ -202,6 +202,7 @@ internal class LoadTimelineStrategy( return firstOrNull()?.let { return TimelineChunk( chunkEntity = it, + timelineSettings = dependencies.timelineSettings, roomId = roomId, timelineId = timelineId, eventDecryptor = dependencies.eventDecryptor, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index fcea10250b..7b45d4cdc5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ChunkEntityFields @@ -46,6 +47,7 @@ private const val PAGINATION_COUNT = 50 * It also triggers pagination to the server when needed, or dispatch to the prev or next chunk if any. */ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, + private val timelineSettings: TimelineSettings, private val roomId: String, private val timelineId: String, private val eventDecryptor: TimelineEventDecryptor, @@ -59,6 +61,9 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, private val isLastForward = AtomicBoolean(chunkEntity.isLastForward) private val chunkObjectListener = RealmObjectChangeListener { _, changeSet -> + if(changeSet?.isDeleted.orFalse()){ + return@RealmObjectChangeListener + } Timber.v("on chunk (${chunkEntity.identifier()}) changed: ${changeSet?.changedFields?.joinToString(",")}") if(changeSet?.isFieldChanged(ChunkEntityFields.IS_LAST_FORWARD).orFalse()){ isLastForward.set(chunkEntity.isLastForward) @@ -252,7 +257,8 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, } private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map( - timelineEventEntity = eventEntity + timelineEventEntity = eventEntity, + buildReadReceipts = timelineSettings.buildReadReceipts ).let { // eventually enhance with ui echo? (uiEchoManager?.decorateEventWithReactionUiEcho(it) ?: it) @@ -261,6 +267,7 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, private fun createTimelineChunk(chunkEntity: ChunkEntity): TimelineChunk { return TimelineChunk( chunkEntity = chunkEntity, + timelineSettings = timelineSettings, timelineId = timelineId, eventDecryptor = eventDecryptor, roomId = roomId, From 03961fe9334406d86b843bb834e03c247a8c507a Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 2 Dec 2021 20:42:29 +0100 Subject: [PATCH 17/29] Timeline: update when loading states changed --- .../android/sdk/api/session/room/timeline/Timeline.kt | 2 +- .../home/room/detail/timeline/TimelineEventController.kt | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt index 3ba2aadd38..c8b14b8f57 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt @@ -95,7 +95,7 @@ interface Timeline { */ fun onNewTimelineEvents(eventIds: List) - fun onStateUpdated() = Unit + fun onStateUpdated(direction: Direction, state: PaginationState) = Unit } /** diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index e5c1cd5f73..706e42f715 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -309,6 +309,14 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // no-op, already handled } + override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) { + if(!state.hasMoreToLoad) { + backgroundHandler.post { + requestModelBuild() + } + } + } + private fun submitSnapshot(newSnapshot: List) { backgroundHandler.post { inSubmitList = true From 76eddef840c320110cac1dc7613ce494507400c7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 2 Dec 2021 20:42:54 +0100 Subject: [PATCH 18/29] Timeline: avoid notifying when decryption error is same as previous --- .../sdk/internal/database/model/EventEntity.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt index 836fc4efaf..ce2d1efc1d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt @@ -40,8 +40,6 @@ internal open class EventEntity(@Index var eventId: String = "", var unsignedData: String? = null, var redacts: String? = null, var decryptionResultJson: String? = null, - var decryptionErrorCode: String? = null, - var decryptionErrorReason: String? = null, var ageLocalTs: Long? = null ) : RealmObject() { @@ -55,6 +53,16 @@ internal open class EventEntity(@Index var eventId: String = "", sendStateStr = value.name } + var decryptionErrorCode: String? = null + set(value) { + if (value != field) field = value + } + + var decryptionErrorReason: String? = null + set(value) { + if (value != field) field = value + } + companion object fun setDecryptionResult(result: MXEventDecryptionResult, clearEvent: JsonDict? = null) { From 014da84ba6a341e9d5088da737eb21b3a687bd99 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 3 Dec 2021 12:14:35 +0100 Subject: [PATCH 19/29] Timeline: try to optimise a bit the loading --- .../session/room/timeline/DefaultTimeline.kt | 21 ++--- .../room/timeline/LoadTimelineStrategy.kt | 5 +- .../session/room/timeline/TimelineChunk.kt | 92 +++++++++++++------ .../timeline/TimelineEventController.kt | 6 +- 4 files changed, 79 insertions(+), 45 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 4332255a8b..37c0f6a44b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.room.timeline import io.realm.Realm import io.realm.RealmConfiguration -import io.realm.RealmResults import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -26,13 +25,11 @@ import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import okhttp3.internal.closeQuietly import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.internal.database.mapper.EventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler @@ -47,16 +44,16 @@ import java.util.concurrent.atomic.AtomicReference internal class DefaultTimeline internal constructor(private val roomId: String, private val initialEventId: String?, - private val settings: TimelineSettings, private val realmConfiguration: RealmConfiguration, private val loadRoomMembersTask: LoadRoomMembersTask, private val readReceiptHandler: ReadReceiptHandler, + settings: TimelineSettings, paginationTask: PaginationTask, getEventTask: GetContextOfEventTask, fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, timelineEventMapper: TimelineEventMapper, timelineInput: TimelineInput, - private val threadsAwarenessHandler: ThreadsAwarenessHandler, + threadsAwarenessHandler: ThreadsAwarenessHandler, eventDecryptor: TimelineEventDecryptor) : Timeline { companion object { @@ -76,6 +73,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, private val sequencer = SemaphoreCoroutineSequencer() private val strategyDependencies = LoadTimelineStrategy.Dependencies( + timelineScope = timelineScope, eventDecryptor = eventDecryptor, timelineSettings = settings, paginationTask = paginationTask, @@ -139,6 +137,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, override fun restartWithEventId(eventId: String?) { timelineScope.launch { openAround(eventId) + postSnapshot() } } @@ -165,7 +164,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, }.get() } - private suspend fun loadMore(count: Long, direction: Timeline.Direction) = withContext(timelineDispatcher) { + private suspend fun loadMore(count: Long, direction: Timeline.Direction) { val baseLogMessage = "loadMore(count: $count, direction: $direction, roomId: $roomId)" Timber.v("$baseLogMessage started") if (!isStarted.get()) { @@ -174,21 +173,21 @@ internal class DefaultTimeline internal constructor(private val roomId: String, val currentState = getPaginationState(direction) if (!currentState.hasMoreToLoad) { Timber.v("$baseLogMessage : nothing more to load") - return@withContext + return } if (currentState.loading) { Timber.v("$baseLogMessage : already loading") - return@withContext + return } updateState(direction) { it.copy(loading = true) } val loadMoreResult = strategy.loadMore(count, direction) + Timber.v("$baseLogMessage: result $loadMoreResult") val hasMoreToLoad = loadMoreResult != LoadMoreResult.REACHED_END updateState(direction) { it.copy(loading = false, hasMoreToLoad = hasMoreToLoad) } - postSnapshot() } private suspend fun openAround(eventId: String?) = withContext(timelineDispatcher) { @@ -210,12 +209,12 @@ internal class DefaultTimeline internal constructor(private val roomId: String, it.copy(loading = false, hasMoreToLoad = true) } strategy.onStart() - postSnapshot() } private fun postSnapshot() { timelineScope.launch { val snapshot = strategy.buildSnapshot() + Timber.v("Post snapshot of ${snapshot.size} items") withContext(Dispatchers.Main) { listeners.forEach { tryOrNull { it.onTimelineUpdated(snapshot) } @@ -242,7 +241,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, stateReference.set(newValue) withContext(Dispatchers.Main) { listeners.forEach { - tryOrNull { it.onStateUpdated() } + tryOrNull { it.onStateUpdated(direction, newValue) } } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index d258dabad9..ea1b26dbdf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -20,6 +20,7 @@ import io.realm.OrderedCollectionChangeSet import io.realm.OrderedRealmCollectionChangeListener import io.realm.Realm import io.realm.RealmResults +import kotlinx.coroutines.CoroutineScope import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline @@ -62,6 +63,7 @@ internal class LoadTimelineStrategy( data class Dependencies( val timelineSettings: TimelineSettings, + val timelineScope: CoroutineScope, val realm: AtomicReference, val eventDecryptor: TimelineEventDecryptor, val paginationTask: PaginationTask, @@ -214,7 +216,8 @@ internal class LoadTimelineStrategy( uiEchoManager = uiEchoManager, threadsAwarenessHandler = dependencies.threadsAwarenessHandler, initialEventId = mode.originEventId(), - onBuiltEvents = dependencies.onEventsUpdated + onBuiltEvents = dependencies.onEventsUpdated, + timelineScope = dependencies.timelineScope ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index 447c4a3aa0..aa2e62fd3a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -22,7 +22,8 @@ import io.realm.RealmObjectChangeListener import io.realm.RealmQuery import io.realm.RealmResults import io.realm.Sort -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.room.timeline.Timeline @@ -50,6 +51,7 @@ private const val PAGINATION_COUNT = 50 * It also triggers pagination to the server when needed, or dispatch to the prev or next chunk if any. */ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, + private val timelineScope: CoroutineScope, private val timelineSettings: TimelineSettings, private val roomId: String, private val timelineId: String, @@ -65,18 +67,30 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, private val isLastForward = AtomicBoolean(chunkEntity.isLastForward) private val chunkObjectListener = RealmObjectChangeListener { _, changeSet -> - if(changeSet?.isDeleted.orFalse()){ + if (changeSet == null) return@RealmObjectChangeListener + if (changeSet.isDeleted.orFalse()) { return@RealmObjectChangeListener } - Timber.v("on chunk (${chunkEntity.identifier()}) changed: ${changeSet?.changedFields?.joinToString(",")}") - if(changeSet?.isFieldChanged(ChunkEntityFields.IS_LAST_FORWARD).orFalse()){ + Timber.v("on chunk (${chunkEntity.identifier()}) changed: ${changeSet.changedFields?.joinToString(",")}") + if (changeSet.isFieldChanged(ChunkEntityFields.IS_LAST_FORWARD)) { isLastForward.set(chunkEntity.isLastForward) } + if (changeSet.isFieldChanged(ChunkEntityFields.NEXT_CHUNK.`$`)) { + nextChunk = createTimelineChunk(chunkEntity.nextChunk) + timelineScope.launch { + nextChunk?.loadMore(PAGINATION_COUNT.toLong(), Timeline.Direction.FORWARDS) + } + } + if (changeSet.isFieldChanged(ChunkEntityFields.PREV_CHUNK.`$`)) { + prevChunk = createTimelineChunk(chunkEntity.prevChunk) + timelineScope.launch { + prevChunk?.loadMore(PAGINATION_COUNT.toLong(), Timeline.Direction.BACKWARDS) + } + } } private val timelineEventCollectionListener = OrderedRealmCollectionChangeListener { results: RealmResults, changeSet: OrderedCollectionChangeSet -> val frozenResults = results.freeze() - Timber.v("on timeline event changed: $changeSet") handleDatabaseChangeSet(frozenResults, changeSet) } @@ -116,18 +130,22 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, suspend fun loadMore(count: Long, direction: Timeline.Direction): LoadMoreResult { val loadFromDbCount = loadFromDb(count, direction) + Timber.v("Has loaded $loadFromDbCount items from db") val offsetCount = count - loadFromDbCount // We have built the right amount of data - if (offsetCount == 0L) { - onBuiltEvents() - return LoadMoreResult.SUCCESS + return if (offsetCount == 0L) { + LoadMoreResult.SUCCESS + } else { + delegateLoadMore(offsetCount, direction) } + } + + private suspend fun delegateLoadMore(offsetCount: Long, direction: Timeline.Direction): LoadMoreResult { return if (direction == Timeline.Direction.FORWARDS) { val nextChunkEntity = chunkEntity.nextChunk if (nextChunkEntity == null) { // Fetch next chunk from server if not in the db - val token = chunkEntity.nextToken - fetchFromServer(token, direction) + fetchFromServer(chunkEntity.nextToken, direction) } else { // otherwise we delegate to the next chunk if (nextChunk == null) { @@ -139,8 +157,7 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, val prevChunkEntity = chunkEntity.prevChunk if (prevChunkEntity == null) { // Fetch prev chunk from server if not in the db - val token = chunkEntity.prevToken - fetchFromServer(token, direction) + fetchFromServer(chunkEntity.prevToken, direction) } else { // otherwise we delegate to the prev chunk if (prevChunk == null) { @@ -227,6 +244,9 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, timelineEventEntities.removeChangeListener(timelineEventCollectionListener) } + /** + * This method tries to read events from the current chunk. + */ private suspend fun loadFromDb(count: Long, direction: Timeline.Direction): Long { val displayIndex = getNextDisplayIndex(direction) ?: return 0 val baseQuery = timelineEventEntities.where() @@ -247,6 +267,7 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, builtEvents.add(timelineEvent) } } + onBuiltEvents() return timelineEvents.size.toLong() } @@ -264,7 +285,6 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList) } - private fun TimelineEventEntity.buildAndDecryptIfNeeded(): TimelineEvent { val timelineEvent = buildTimelineEvent(this) val transactionId = timelineEvent.root.unsignedData?.transactionId @@ -284,23 +304,11 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, (uiEchoManager?.decorateEventWithReactionUiEcho(it) ?: it) } - private fun createTimelineChunk(chunkEntity: ChunkEntity): TimelineChunk { - return TimelineChunk( - chunkEntity = chunkEntity, - timelineSettings = timelineSettings, - timelineId = timelineId, - eventDecryptor = eventDecryptor, - roomId = roomId, - paginationTask = paginationTask, - fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, - timelineEventMapper = timelineEventMapper, - uiEchoManager = uiEchoManager, - threadsAwarenessHandler = threadsAwarenessHandler, - initialEventId = null, - onBuiltEvents = onBuiltEvents - ) - } - + /** + * Will try to fetch a new chunk on the home server. + * It will take care to update the database by inserting new events and linking new chunk + * with this one. + */ private suspend fun fetchFromServer(token: String?, direction: Timeline.Direction): LoadMoreResult { val paginationResult = try { if (token == null) { @@ -309,6 +317,7 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, val taskParams = FetchTokenAndPaginateTask.Params(roomId, lastKnownEventId, direction.toPaginationDirection(), PAGINATION_COUNT) fetchTokenAndPaginateTask.execute(taskParams) } else { + Timber.v("Fetch more events on server") val taskParams = PaginationTask.Params(roomId, token, direction.toPaginationDirection(), PAGINATION_COUNT) paginationTask.execute(taskParams) } @@ -337,6 +346,10 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, return offset } + /** + * This method is responsible for managing insertions and updates of events on this chunk. + * + */ private fun handleDatabaseChangeSet(frozenResults: RealmResults, changeSet: OrderedCollectionChangeSet) { val insertions = changeSet.insertionRanges for (range in insertions) { @@ -385,6 +398,25 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, builtEvents.last().displayIndex - 1 } } + + private fun createTimelineChunk(chunkEntity: ChunkEntity?): TimelineChunk? { + if (chunkEntity == null) return null + return TimelineChunk( + chunkEntity = chunkEntity, + timelineScope = timelineScope, + timelineSettings = timelineSettings, + timelineId = timelineId, + eventDecryptor = eventDecryptor, + roomId = roomId, + paginationTask = paginationTask, + fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, + timelineEventMapper = timelineEventMapper, + uiEchoManager = uiEchoManager, + threadsAwarenessHandler = threadsAwarenessHandler, + initialEventId = null, + onBuiltEvents = this.onBuiltEvents + ) + } } private fun RealmQuery.offsets( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 706e42f715..78463fd0c1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -310,9 +310,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) { - if(!state.hasMoreToLoad) { + if (!state.hasMoreToLoad) { backgroundHandler.post { - requestModelBuild() + requestDelayedModelBuild(0) } } } @@ -324,7 +324,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec currentSnapshot = newSnapshot val diffResult = DiffUtil.calculateDiff(diffCallback) diffResult.dispatchUpdatesTo(listUpdateCallback) - requestModelBuild() + requestDelayedModelBuild(0) inSubmitList = false } } From 7fa4bf182ab07b2fd34c6db7881a5a999f231fdf Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 3 Dec 2021 12:15:04 +0100 Subject: [PATCH 20/29] Timeline: get off main thread when waiting for positionOrReadMarker --- .../home/room/detail/RoomDetailFragment.kt | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 08a2e6cd9c..dddd8ce03f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -184,11 +184,13 @@ import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.WidgetArgs import im.vector.app.features.widgets.WidgetKind import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import nl.dionsegijn.konfetti.models.Shape import nl.dionsegijn.konfetti.models.Size @@ -1298,27 +1300,28 @@ class RoomDetailFragment @Inject constructor( private fun updateJumpToReadMarkerViewVisibility() { viewLifecycleOwner.lifecycleScope.launchWhenResumed { - withState(roomDetailViewModel) { - val showJumpToUnreadBanner = when (it.unreadState) { - UnreadState.Unknown, - UnreadState.HasNoUnread -> false - is UnreadState.ReadMarkerNotLoaded -> true - is UnreadState.HasUnread -> { - if (it.canShowJumpToReadMarker) { - val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition() - val positionOfReadMarker = timelineEventController.getPositionOfReadMarker() - if (positionOfReadMarker == null) { - false - } else { - positionOfReadMarker > lastVisibleItem - } - } else { - false + val state = roomDetailViewModel.awaitState() + val showJumpToUnreadBanner = when (state.unreadState) { + UnreadState.Unknown, + UnreadState.HasNoUnread -> false + is UnreadState.ReadMarkerNotLoaded -> true + is UnreadState.HasUnread -> { + if (state.canShowJumpToReadMarker) { + val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition() + val positionOfReadMarker = withContext(Dispatchers.Default) { + timelineEventController.getPositionOfReadMarker() } + if (positionOfReadMarker == null) { + false + } else { + positionOfReadMarker > lastVisibleItem + } + } else { + false } } - views.jumpToReadMarkerView.isVisible = showJumpToUnreadBanner } + views.jumpToReadMarkerView.isVisible = showJumpToUnreadBanner } } From 3217277bc45a5da6964d75a42a7cadfe663821cc Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 3 Dec 2021 18:39:04 +0100 Subject: [PATCH 21/29] Timeline: check for create event to hide loader --- .../sdk/api/session/room/timeline/Timeline.kt | 9 +++++--- .../timeline/TimelineEventController.kt | 23 +++++-------------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt index c8b14b8f57..548c639231 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt @@ -83,18 +83,21 @@ interface Timeline { * The latest event is the first in the list * @param snapshot the most up to date snapshot */ - fun onTimelineUpdated(snapshot: List) + fun onTimelineUpdated(snapshot: List) = Unit /** * Called whenever an error we can't recover from occurred */ - fun onTimelineFailure(throwable: Throwable) + fun onTimelineFailure(throwable: Throwable) = Unit /** * Called when new events come through the sync */ - fun onNewTimelineEvents(eventIds: List) + fun onNewTimelineEvents(eventIds: List) = Unit + /** + * Called when the pagination state has changed in one direction + */ fun onStateUpdated(direction: Direction, state: PaginationState) = Unit } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 78463fd0c1..ac6de2ebb0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -174,6 +174,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private var inSubmitList: Boolean = false private var hasReachedInvite: Boolean = false private var hasUTD: Boolean = false + private var hasReachedCreateEvent: Boolean = false private var positionOfReadMarker: Int? = null private var partialState: PartialState = PartialState() @@ -286,7 +287,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec return } // Avoid displaying two loaders if there is no elements between them - val showBackwardsLoader = !showingForwardLoader || timelineModels.isNotEmpty() + val showBackwardsLoader = (!showingForwardLoader || timelineModels.isNotEmpty()) && !hasReachedCreateEvent // We can hide the loader but still add the item to controller so it can trigger backwards pagination LoadingItem_() .id("backward_loading_item_$timestamp") @@ -301,22 +302,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec submitSnapshot(snapshot) } - override fun onTimelineFailure(throwable: Throwable) { - // no-op, already handled - } - - override fun onNewTimelineEvents(eventIds: List) { - // no-op, already handled - } - - override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) { - if (!state.hasMoreToLoad) { - backgroundHandler.post { - requestDelayedModelBuild(0) - } - } - } - private fun submitSnapshot(newSnapshot: List) { backgroundHandler.post { inSubmitList = true @@ -471,10 +456,14 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private fun preprocessReverseEvents() { receiptsByEvent.clear() timelineEventsGroups.clear() + hasReachedCreateEvent = false val itr = currentSnapshot.listIterator(currentSnapshot.size) var lastShownEventId: String? = null while (itr.hasPrevious()) { val event = itr.previous() + if (!hasReachedCreateEvent && event.root.type == EventType.STATE_ROOM_CREATE) { + hasReachedCreateEvent = true + } timelineEventsGroups.addOrIgnore(event) val currentReadReceipts = ArrayList(event.readReceipts).filter { it.user.userId != session.myUserId From 29a4fd1e419e3d6b3fab63f168388271a3a1b258 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 7 Dec 2021 21:13:41 +0100 Subject: [PATCH 22/29] Timeline: make 3 integration tests passing (also add some suspend method on the timeline) --- .../android/sdk/common/CommonTestHelper.kt | 69 +++---- .../TimelineBackToPreviousLastForwardTest.kt | 183 ------------------ .../timeline/TimelineForwardPaginationTest.kt | 55 ++---- .../TimelinePreviousLastForwardTest.kt | 4 +- .../TimelineSimpleBackPaginationTest.kt | 105 ++++++++++ .../sdk/session/room/timeline/TimelineTest.kt | 84 -------- .../sdk/api/session/room/timeline/Timeline.kt | 15 ++ .../session/room/timeline/DefaultTimeline.kt | 60 ++++-- .../room/timeline/LoadTimelineStrategy.kt | 36 ++-- .../session/room/timeline/TimelineChunk.kt | 135 ++++++++----- .../timeline/TimelineEventController.kt | 11 +- 11 files changed, 331 insertions(+), 426 deletions(-) delete mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt delete mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineTest.kt diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index 8e21828562..3cb699378f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -145,36 +145,9 @@ class CommonTestHelper(context: Context) { * @param nbOfMessages the number of time the message will be sent */ fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List { - val sentEvents = ArrayList(nbOfMessages) val timeline = room.createTimeline(null, TimelineSettings(10)) timeline.start() - waitWithLatch(timeout + 1_000L * nbOfMessages) { latch -> - val timelineListener = object : Timeline.Listener { - override fun onTimelineFailure(throwable: Throwable) { - } - - override fun onNewTimelineEvents(eventIds: List) { - // noop - } - - override fun onTimelineUpdated(snapshot: List) { - val newMessages = snapshot - .filter { it.root.sendState == SendState.SYNCED } - .filter { it.root.getClearType() == EventType.MESSAGE } - .filter { it.root.getClearContent().toModel()?.body?.startsWith(message) == true } - - Timber.v("New synced message size: ${newMessages.size}") - if (newMessages.size == nbOfMessages) { - sentEvents.addAll(newMessages) - // Remove listener now, if not at the next update sendEvents could change - timeline.removeListener(this) - latch.countDown() - } - } - } - timeline.addListener(timelineListener) - sendTextMessagesBatched(room, message, nbOfMessages) - } + val sentEvents = sendTextMessagesBatched(timeline, room, message, nbOfMessages, timeout) timeline.dispose() // Check that all events has been created assertEquals("Message number do not match $sentEvents", nbOfMessages.toLong(), sentEvents.size.toLong()) @@ -182,9 +155,10 @@ class CommonTestHelper(context: Context) { } /** - * Will send nb of messages provided by count parameter but waits a bit every 10 messages to avoid gap in sync + * Will send nb of messages provided by count parameter but waits every 10 messages to avoid gap in sync */ - private fun sendTextMessagesBatched(room: Room, message: String, count: Int) { + private fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long): List { + val sentEvents = ArrayList(count) (1 until count + 1) .map { "$message #$it" } .chunked(10) @@ -192,8 +166,34 @@ class CommonTestHelper(context: Context) { batchedMessages.forEach { formattedMessage -> room.sendTextMessage(formattedMessage) } - Thread.sleep(1_000L) + waitWithLatch(timeout) { latch -> + val timelineListener = object : Timeline.Listener { + + override fun onTimelineUpdated(snapshot: List) { + val allSentMessages = snapshot + .filter { it.root.sendState == SendState.SYNCED } + .filter { it.root.getClearType() == EventType.MESSAGE } + .filter { it.root.getClearContent().toModel()?.body?.startsWith(message) == true } + + val hasSyncedAllBatchedMessages = allSentMessages + .map { + it.root.getClearContent().toModel()?.body + } + .containsAll(batchedMessages) + + if (allSentMessages.size == count) { + sentEvents.addAll(allSentMessages) + } + if (hasSyncedAllBatchedMessages) { + timeline.removeListener(this) + latch.countDown() + } + } + } + timeline.addListener(timelineListener) + } } + return sentEvents } // PRIVATE METHODS ***************************************************************************** @@ -332,13 +332,6 @@ class CommonTestHelper(context: Context) { fun createEventListener(latch: CountDownLatch, predicate: (List) -> Boolean): Timeline.Listener { return object : Timeline.Listener { - override fun onTimelineFailure(throwable: Throwable) { - // noop - } - - override fun onNewTimelineEvents(eventIds: List) { - // noop - } override fun onTimelineUpdated(snapshot: List) { if (predicate(snapshot)) { diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt deleted file mode 100644 index 7628f287c9..0000000000 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineBackToPreviousLastForwardTest.kt +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.session.room.timeline - -import org.amshove.kluent.shouldBeFalse -import org.amshove.kluent.shouldBeTrue -import org.junit.Assert.assertTrue -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.junit.runners.MethodSorters -import org.matrix.android.sdk.InstrumentedTest -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.api.session.room.timeline.Timeline -import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.common.CommonTestHelper -import org.matrix.android.sdk.common.CryptoTestHelper -import org.matrix.android.sdk.common.checkSendOrder -import timber.log.Timber -import java.util.concurrent.CountDownLatch - -@RunWith(JUnit4::class) -@FixMethodOrder(MethodSorters.JVM) -class TimelineBackToPreviousLastForwardTest : InstrumentedTest { - - private val commonTestHelper = CommonTestHelper(context()) - private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) - - /** - * This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink of an - * even contained in a previous lastForward chunk, we will be able to go back to the live - */ - @Test - fun backToPreviousLastForwardTest() { - val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) - - val aliceSession = cryptoTestData.firstSession - val bobSession = cryptoTestData.secondSession!! - val aliceRoomId = cryptoTestData.roomId - - aliceSession.cryptoService().setWarnOnUnknownDevices(false) - bobSession.cryptoService().setWarnOnUnknownDevices(false) - - val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!! - val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!! - - val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30)) - bobTimeline.start() - - var roomCreationEventId: String? = null - - run { - val lock = CountDownLatch(1) - val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> - Timber.e("Bob timeline updated: with ${snapshot.size} events:") - snapshot.forEach { - Timber.w(" event ${it.root}") - } - - roomCreationEventId = snapshot.lastOrNull()?.root?.eventId - // Ok, we have the 8 first messages of the initial sync (room creation and bob join event) - snapshot.size == 8 - } - - bobTimeline.addListener(eventsListener) - commonTestHelper.await(lock) - bobTimeline.removeAllListeners() - - bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() - bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() - } - - // Bob stop to sync - bobSession.stopSync() - - val messageRoot = "First messages from Alice" - - // Alice sends 30 messages - commonTestHelper.sendTextMessage( - roomFromAlicePOV, - messageRoot, - 30) - - // Bob start to sync - bobSession.startSync(true) - - run { - val lock = CountDownLatch(1) - val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> - Timber.e("Bob timeline updated: with ${snapshot.size} events:") - snapshot.forEach { - Timber.w(" event ${it.root}") - } - - // Ok, we have the 10 last messages from Alice. - snapshot.size == 10 && - snapshot.all { it.root.content.toModel()?.body?.startsWith(messageRoot).orFalse() } - } - - bobTimeline.addListener(eventsListener) - commonTestHelper.await(lock) - bobTimeline.removeAllListeners() - - bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue() - bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() - } - - // Bob navigate to the first event (room creation event), so inside the previous last forward chunk - run { - val lock = CountDownLatch(1) - val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> - Timber.e("Bob timeline updated: with ${snapshot.size} events:") - snapshot.forEach { - Timber.w(" event ${it.root}") - } - - // The event is in db, so it is fetch and auto pagination occurs, half of the number of events we have for this chunk (?) - snapshot.size == 4 - } - - bobTimeline.addListener(eventsListener) - - // Restart the timeline to the first sent event, which is already in the database, so pagination should start automatically - assertTrue(roomFromBobPOV.getTimeLineEvent(roomCreationEventId!!) != null) - - bobTimeline.restartWithEventId(roomCreationEventId) - - commonTestHelper.await(lock) - bobTimeline.removeAllListeners() - - bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() - bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() - } - - // Bob scroll to the future - run { - val lock = CountDownLatch(1) - val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> - Timber.e("Bob timeline updated: with ${snapshot.size} events:") - snapshot.forEach { - Timber.w(" event ${it.root}") - } - - // Bob can see the first event of the room (so Back pagination has worked) - snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE && - // 8 for room creation item, and 30 for the forward pagination - snapshot.size == 38 && - snapshot.checkSendOrder(messageRoot, 30, 0) - } - - bobTimeline.addListener(eventsListener) - - bobTimeline.paginate(Timeline.Direction.FORWARDS, 50) - - commonTestHelper.await(lock) - bobTimeline.removeAllListeners() - - bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() - bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() - } - bobTimeline.dispose() - - cryptoTestData.cleanUp(commonTestHelper) - } -} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt index bc9722c922..05a43de0ac 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.session.room.timeline +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeTrue import org.junit.FixMethodOrder @@ -123,54 +125,29 @@ class TimelineForwardPaginationTest : InstrumentedTest { // Alice paginates BACKWARD and FORWARD of 50 events each // Then she can only navigate FORWARD run { - val lock = CountDownLatch(1) - val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> - Timber.e("Alice timeline updated: with ${snapshot.size} events:") - snapshot.forEach { - Timber.w(" event ${it.root.content}") - } - - // Alice can see the first event of the room (so Back pagination has worked) - snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE && - // 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination - snapshot.size == 57 // 6 + 1 + 50 + val snapshot = runBlocking { + aliceTimeline.awaitPaginate(Timeline.Direction.BACKWARDS, 50) + aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50) } - - aliceTimeline.addListener(aliceEventsListener) - - // Restart the timeline to the first sent event - // We ask to load event backward and forward - aliceTimeline.paginate(Timeline.Direction.BACKWARDS, 50) - aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50) - - commonTestHelper.await(lock) - aliceTimeline.removeAllListeners() - aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue() aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse() + + assertEquals(EventType.STATE_ROOM_CREATE, snapshot.lastOrNull()?.root?.getClearType()) + // 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination + // 6 + 1 + 50 + assertEquals(57, snapshot.size) } // Alice paginates once again FORWARD for 50 events // All the timeline is retrieved, she cannot paginate anymore in both direction run { - val lock = CountDownLatch(1) - val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot -> - Timber.e("Alice timeline updated: with ${snapshot.size} events:") - snapshot.forEach { - Timber.w(" event ${it.root.content}") - } - // 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room) - snapshot.size == 6 + numberOfMessagesToSend && - snapshot.checkSendOrder(message, numberOfMessagesToSend, 0) - } - - aliceTimeline.addListener(aliceEventsListener) - // Ask for a forward pagination - aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50) - - commonTestHelper.await(lock) - aliceTimeline.removeAllListeners() + val snapshot = runBlocking { + aliceTimeline.awaitPaginate(Timeline.Direction.FORWARDS, 50) + } + // 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room) + snapshot.size == 6 + numberOfMessagesToSend && + snapshot.checkSendOrder(message, numberOfMessagesToSend, 0) // The timeline is fully loaded aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt index e865fe17da..c6fdec150d 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelinePreviousLastForwardTest.kt @@ -168,10 +168,8 @@ class TimelinePreviousLastForwardTest : InstrumentedTest { bobTimeline.addListener(eventsListener) - // Restart the timeline to the first sent event, and paginate in both direction + // Restart the timeline to the first sent event bobTimeline.restartWithEventId(firstMessageFromAliceId) - bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50) - bobTimeline.paginate(Timeline.Direction.FORWARDS, 50) commonTestHelper.await(lock) bobTimeline.removeAllListeners() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt new file mode 100644 index 0000000000..bec6886fb1 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.session.room.timeline + +import kotlinx.coroutines.runBlocking +import org.amshove.kluent.internal.assertEquals +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.events.model.isTextMessage +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import org.matrix.android.sdk.common.TestConstants + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class TimelineSimpleBackPaginationTest : InstrumentedTest { + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + @Test + fun timeline_backPaginate_shouldReachEndOfTimeline() { + + val numberOfMessagesToSent = 200 + + val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + val roomId = cryptoTestData.roomId + + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + bobSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomFromAlicePOV = aliceSession.getRoom(roomId)!! + val roomFromBobPOV = bobSession.getRoom(roomId)!! + + // Alice sends X messages + val message = "Message from Alice" + commonTestHelper.sendTextMessage( + roomFromAlicePOV, + message, + numberOfMessagesToSent) + + val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30)) + bobTimeline.start() + + commonTestHelper.waitWithLatch(timeout = TestConstants.timeOutMillis * 10) { + + val listener = object : Timeline.Listener { + + override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) { + if (direction == Timeline.Direction.FORWARDS) + return + if (state.hasMoreToLoad && !state.loading) { + bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) + } else if (!state.hasMoreToLoad) { + bobTimeline.removeListener(this) + it.countDown() + } + } + } + bobTimeline.addListener(listener) + bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) + } + assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS)) + assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS)) + + val onlySentEvents = runBlocking { + bobTimeline.awaitSnapshot() + } + .filter { + it.root.isTextMessage() + }.filter { + (it.root.content.toModel())?.body?.startsWith(message).orFalse() + } + assertEquals(numberOfMessagesToSent, onlySentEvents.size) + + bobTimeline.dispose() + cryptoTestData.cleanUp(commonTestHelper) + } +} diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineTest.kt deleted file mode 100644 index 9be0a5d5af..0000000000 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineTest.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.session.room.timeline - -import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.InstrumentedTest - -internal class TimelineTest : InstrumentedTest { - - companion object { - private const val ROOM_ID = "roomId" - } - - private lateinit var monarchy: Monarchy - -// @Before -// fun setup() { -// Timber.plant(Timber.DebugTree()) -// Realm.init(context()) -// val testConfiguration = RealmConfiguration.Builder().name("test-realm") -// .modules(SessionRealmModule()).build() -// -// Realm.deleteRealm(testConfiguration) -// monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build() -// RoomDataHelper.fakeInitialSync(monarchy, ROOM_ID) -// } -// -// private fun createTimeline(initialEventId: String? = null): Timeline { -// val taskExecutor = TaskExecutor(testCoroutineDispatchers) -// val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy) -// val paginationTask = FakePaginationTask @Inject constructor(tokenChunkEventPersistor) -// val getContextOfEventTask = FakeGetContextOfEventTask @Inject constructor(tokenChunkEventPersistor) -// val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID) -// val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, EventRelationExtractor()) -// return DefaultTimeline( -// ROOM_ID, -// initialEventId, -// monarchy.realmConfiguration, -// taskExecutor, -// getContextOfEventTask, -// timelineEventFactory, -// paginationTask, -// null) -// } -// -// @Test -// fun backPaginate_shouldLoadMoreEvents_whenPaginateIsCalled() { -// val timeline = createTimeline() -// timeline.start() -// val paginationCount = 30 -// var initialLoad = 0 -// val latch = CountDownLatch(2) -// var timelineEvents: List = emptyList() -// timeline.listener = object : Timeline.Listener { -// override fun onTimelineUpdated(snapshot: List) { -// if (snapshot.isNotEmpty()) { -// if (initialLoad == 0) { -// initialLoad = snapshot.size -// } -// timelineEvents = snapshot -// latch.countDown() -// timeline.paginate(Timeline.Direction.BACKWARDS, paginationCount) -// } -// } -// } -// latch.await() -// timelineEvents.size shouldBeEqualTo initialLoad + paginationCount -// timeline.dispose() -// } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt index 548c639231..443b1a8f13 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt @@ -70,13 +70,28 @@ interface Timeline { */ fun paginate(direction: Direction, count: Int) + /** + * This is the same than the regular paginate method but waits for the results instead + * of relying on the timeline listener. + * Note that it will still trigger onTimelineUpdated internally. + */ + suspend fun awaitPaginate(direction: Direction, count: Int): List + /** * Returns the index of a built event or null. */ fun getIndexOfEvent(eventId: String?): Int? + /** + * Returns the current pagination state for the direction. + */ fun getPaginationState(direction: Direction): PaginationState + /** + * Returns a snapshot of the timeline in his current state. + */ + suspend fun awaitSnapshot(): List + interface Listener { /** * Call when the timeline has been updated through pagination or sync. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 37c0f6a44b..5e9efecc67 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.withContext import okhttp3.internal.closeQuietly import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask @@ -47,7 +48,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, private val realmConfiguration: RealmConfiguration, private val loadRoomMembersTask: LoadRoomMembersTask, private val readReceiptHandler: ReadReceiptHandler, - settings: TimelineSettings, + private val settings: TimelineSettings, paginationTask: PaginationTask, getEventTask: GetContextOfEventTask, fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, @@ -83,6 +84,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, realm = backgroundRealm, getContextOfEventTask = getEventTask, threadsAwarenessHandler = threadsAwarenessHandler, + onLimitedTimeline = this::onLimitedTimeline, onEventsUpdated = this::postSnapshot, onNewTimelineEvents = this::onNewTimelineEvents ) @@ -108,7 +110,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, override fun start() { timelineScope.launch { - loadRoomMemberIfNeeded() + loadRoomMembersIfNeeded() } timelineScope.launch { sequencer.post { @@ -147,10 +149,21 @@ internal class DefaultTimeline internal constructor(private val roomId: String, override fun paginate(direction: Timeline.Direction, count: Int) { timelineScope.launch { - loadMore(count.toLong(), direction) + loadMore(count, direction, fetchOnServerIfNeeded = true) } } + override suspend fun awaitPaginate(direction: Timeline.Direction, count: Int): List { + withContext(timelineDispatcher) { + loadMore(count, direction, fetchOnServerIfNeeded = true) + } + return awaitSnapshot() + } + + override suspend fun awaitSnapshot(): List = withContext(timelineDispatcher) { + strategy.buildSnapshot() + } + override fun getIndexOfEvent(eventId: String?): Int? { if (eventId == null) return null return strategy.getBuiltEventIndex(eventId) @@ -164,8 +177,8 @@ internal class DefaultTimeline internal constructor(private val roomId: String, }.get() } - private suspend fun loadMore(count: Long, direction: Timeline.Direction) { - val baseLogMessage = "loadMore(count: $count, direction: $direction, roomId: $roomId)" + private suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean) { + val baseLogMessage = "loadMore(count: $count, direction: $direction, roomId: $roomId, fetchOnServer: $fetchOnServerIfNeeded)" Timber.v("$baseLogMessage started") if (!isStarted.get()) { throw IllegalStateException("You should call start before using timeline") @@ -182,7 +195,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, updateState(direction) { it.copy(loading = true) } - val loadMoreResult = strategy.loadMore(count, direction) + val loadMoreResult = strategy.loadMore(count, direction, fetchOnServerIfNeeded) Timber.v("$baseLogMessage: result $loadMoreResult") val hasMoreToLoad = loadMoreResult != LoadMoreResult.REACHED_END updateState(direction) { @@ -202,13 +215,29 @@ internal class DefaultTimeline internal constructor(private val roomId: String, } else { buildStrategy(LoadTimelineStrategy.Mode.Permalink(eventId)) } + initPaginationStates(eventId) + strategy.onStart() + loadMore( + count = strategyDependencies.timelineSettings.initialSize, + direction = Timeline.Direction.BACKWARDS, + fetchOnServerIfNeeded = false + ) + } + + private suspend fun initPaginationStates(eventId: String?) { updateState(Timeline.Direction.FORWARDS) { it.copy(loading = false, hasMoreToLoad = eventId != null) } updateState(Timeline.Direction.BACKWARDS) { it.copy(loading = false, hasMoreToLoad = true) } - strategy.onStart() + } + + private fun onLimitedTimeline() { + timelineScope.launch { + initPaginationStates(null) + loadMore(settings.initialSize, Timeline.Direction.BACKWARDS, false) + } } private fun postSnapshot() { @@ -239,10 +268,15 @@ internal class DefaultTimeline internal constructor(private val roomId: String, val currentValue = stateReference.get() val newValue = update(currentValue) stateReference.set(newValue) - withContext(Dispatchers.Main) { - listeners.forEach { - tryOrNull { it.onStateUpdated(direction, newValue) } - } + if (newValue != currentValue) { + postPaginationState(direction, newValue) + } + } + + private suspend fun postPaginationState(direction: Timeline.Direction, state: Timeline.PaginationState) = withContext(Dispatchers.Main) { + Timber.v("Post $direction pagination state: $state ") + listeners.forEach { + tryOrNull { it.onStateUpdated(direction, state) } } } @@ -255,14 +289,14 @@ internal class DefaultTimeline internal constructor(private val roomId: String, ) } - private suspend fun loadRoomMemberIfNeeded() { + private suspend fun loadRoomMembersIfNeeded() { val loadRoomMembersParam = LoadRoomMembersTask.Params(roomId) try { loadRoomMembersTask.execute(loadRoomMembersParam) } catch (failure: Throwable) { Timber.v("Failed to load room members. Retry in 10s.") delay(10_000L) - loadRoomMemberIfNeeded() + loadRoomMembersIfNeeded() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index ea1b26dbdf..fb0971ad47 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -20,7 +20,9 @@ import io.realm.OrderedCollectionChangeSet import io.realm.OrderedRealmCollectionChangeListener import io.realm.Realm import io.realm.RealmResults +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline @@ -73,18 +75,27 @@ internal class LoadTimelineStrategy( val timelineEventMapper: TimelineEventMapper, val threadsAwarenessHandler: ThreadsAwarenessHandler, val onEventsUpdated: () -> Unit, + val onLimitedTimeline: () -> Unit, val onNewTimelineEvents: (List) -> Unit ) + private var getContextLatch: CompletableDeferred? = null private var chunkEntity: RealmResults? = null private var timelineChunk: TimelineChunk? = null private val chunkEntityListener = OrderedRealmCollectionChangeListener { _: RealmResults, changeSet: OrderedCollectionChangeSet -> + // Can be call either when you open a permalink on an unknown event + // or when there is a gap in the timeline. val shouldRebuildChunk = changeSet.insertions.isNotEmpty() if (shouldRebuildChunk) { timelineChunk?.close(closeNext = true, closePrev = true) timelineChunk = chunkEntity?.createTimelineChunk() - dependencies.onEventsUpdated() + // If we are waiting for a result of get context, post completion + getContextLatch?.complete(Unit) + // If we have a gap, just tell the timeline about it. + if (timelineChunk?.hasReachedLastForward().orFalse()) { + dependencies.onLimitedTimeline() + } } } @@ -95,6 +106,7 @@ internal class LoadTimelineStrategy( } private val timelineInputListener = object : TimelineInput.Listener { + override fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) { if (roomId != this@LoadTimelineStrategy.roomId) { return @@ -130,7 +142,7 @@ internal class LoadTimelineStrategy( onEventsUpdated = dependencies.onEventsUpdated ) - suspend fun onStart() { + fun onStart() { dependencies.eventDecryptor.start() dependencies.timelineInput.listeners.add(timelineInputListener) val realm = dependencies.realm.get() @@ -139,9 +151,6 @@ internal class LoadTimelineStrategy( it.addChangeListener(chunkEntityListener) timelineChunk = it.createTimelineChunk() } - if (mode is Mode.Live) { - loadMore(dependencies.timelineSettings.initialSize.toLong(), Timeline.Direction.BACKWARDS) - } } fun onStop() { @@ -150,22 +159,25 @@ internal class LoadTimelineStrategy( chunkEntity?.removeChangeListener(chunkEntityListener) sendingEventsDataSource.stop() timelineChunk?.close(closeNext = true, closePrev = true) + getContextLatch?.cancel() chunkEntity = null timelineChunk = null } - suspend fun loadMore(count: Long, direction: Timeline.Direction): LoadMoreResult { - return if (mode is Mode.Permalink && timelineChunk == null) { + suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult { + if (mode is Mode.Permalink && timelineChunk == null) { val params = GetContextOfEventTask.Params(roomId, mode.originEventId) try { + getContextLatch = CompletableDeferred() dependencies.getContextOfEventTask.execute(params) - LoadMoreResult.SUCCESS + // waits for the query to be fulfilled + getContextLatch?.await() + getContextLatch = null } catch (failure: Throwable) { - LoadMoreResult.FAILURE + return LoadMoreResult.FAILURE } - } else { - timelineChunk?.loadMore(count, direction) ?: LoadMoreResult.FAILURE } + return timelineChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE } fun getBuiltEventIndex(eventId: String): Int? { @@ -198,7 +210,7 @@ internal class LoadTimelineStrategy( } } - private fun hasReachedLastForward(): Boolean{ + private fun hasReachedLastForward(): Boolean { return timelineChunk?.hasReachedLastForward().orFalse() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index aa2e62fd3a..78bc02f9c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -22,10 +22,11 @@ import io.realm.RealmObjectChangeListener import io.realm.RealmQuery import io.realm.RealmResults import io.realm.Sort +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings @@ -40,11 +41,6 @@ import timber.log.Timber import java.util.Collections import java.util.concurrent.atomic.AtomicBoolean -/** - * This is the value used to fetch on server. It's better to make constant as otherwise we can have weird chunks with disparate and small chunk of data. - */ -private const val PAGINATION_COUNT = 50 - /** * This is a wrapper around a ChunkEntity in the database. * It does mainly listen to the db timeline events. @@ -65,6 +61,9 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, private val onBuiltEvents: () -> Unit) { private val isLastForward = AtomicBoolean(chunkEntity.isLastForward) + private val isLastBackward = AtomicBoolean(chunkEntity.isLastBackward) + private var prevChunkLatch: CompletableDeferred? = null + private var nextChunkLatch: CompletableDeferred? = null private val chunkObjectListener = RealmObjectChangeListener { _, changeSet -> if (changeSet == null) return@RealmObjectChangeListener @@ -75,17 +74,16 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, if (changeSet.isFieldChanged(ChunkEntityFields.IS_LAST_FORWARD)) { isLastForward.set(chunkEntity.isLastForward) } + if (changeSet.isFieldChanged(ChunkEntityFields.IS_LAST_BACKWARD)) { + isLastBackward.set(chunkEntity.isLastBackward) + } if (changeSet.isFieldChanged(ChunkEntityFields.NEXT_CHUNK.`$`)) { nextChunk = createTimelineChunk(chunkEntity.nextChunk) - timelineScope.launch { - nextChunk?.loadMore(PAGINATION_COUNT.toLong(), Timeline.Direction.FORWARDS) - } + nextChunkLatch?.complete(Unit) } if (changeSet.isFieldChanged(ChunkEntityFields.PREV_CHUNK.`$`)) { prevChunk = createTimelineChunk(chunkEntity.prevChunk) - timelineScope.launch { - prevChunk?.loadMore(PAGINATION_COUNT.toLong(), Timeline.Direction.BACKWARDS) - } + prevChunkLatch?.complete(Unit) } } @@ -128,42 +126,63 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, return deepBuiltItems } - suspend fun loadMore(count: Long, direction: Timeline.Direction): LoadMoreResult { - val loadFromDbCount = loadFromDb(count, direction) - Timber.v("Has loaded $loadFromDbCount items from db") + /** + * This will take care of loading and building events of this chunk for the given direction and count. + * If @param fetchFromServerIfNeeded is true, it will try to fetch more events on server to get the right amount of data. + * This method will also post a snapshot as soon the data is built from db to avoid waiting for server response. + */ + suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult { + if (direction == Timeline.Direction.FORWARDS && nextChunk != null) { + return nextChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE + } else if (direction == Timeline.Direction.BACKWARDS && prevChunk != null) { + return prevChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE + } + val loadFromDbCount = loadFromStorage(count, direction) + Timber.v("Has loaded $loadFromDbCount items from storage") val offsetCount = count - loadFromDbCount - // We have built the right amount of data - return if (offsetCount == 0L) { + return if (direction == Timeline.Direction.FORWARDS && isLastForward.get()) { + LoadMoreResult.REACHED_END + } else if (direction == Timeline.Direction.BACKWARDS && isLastBackward.get()) { + LoadMoreResult.REACHED_END + } else if (offsetCount == 0) { LoadMoreResult.SUCCESS } else { - delegateLoadMore(offsetCount, direction) + delegateLoadMore(fetchOnServerIfNeeded, offsetCount, direction) } } - private suspend fun delegateLoadMore(offsetCount: Long, direction: Timeline.Direction): LoadMoreResult { + private suspend fun delegateLoadMore(fetchFromServerIfNeeded: Boolean, offsetCount: Int, direction: Timeline.Direction): LoadMoreResult { return if (direction == Timeline.Direction.FORWARDS) { val nextChunkEntity = chunkEntity.nextChunk - if (nextChunkEntity == null) { - // Fetch next chunk from server if not in the db - fetchFromServer(chunkEntity.nextToken, direction) - } else { - // otherwise we delegate to the next chunk - if (nextChunk == null) { - nextChunk = createTimelineChunk(nextChunkEntity) + when { + nextChunkEntity != null -> { + if (nextChunk == null) { + nextChunk = createTimelineChunk(nextChunkEntity) + } + nextChunk?.loadMore(offsetCount, direction, fetchFromServerIfNeeded) ?: LoadMoreResult.FAILURE + } + fetchFromServerIfNeeded -> { + fetchFromServer(offsetCount, chunkEntity.nextToken, direction) + } + else -> { + LoadMoreResult.SUCCESS } - nextChunk?.loadMore(offsetCount, direction) ?: LoadMoreResult.FAILURE } } else { val prevChunkEntity = chunkEntity.prevChunk - if (prevChunkEntity == null) { - // Fetch prev chunk from server if not in the db - fetchFromServer(chunkEntity.prevToken, direction) - } else { - // otherwise we delegate to the prev chunk - if (prevChunk == null) { - prevChunk = createTimelineChunk(prevChunkEntity) + when { + prevChunkEntity != null -> { + if (prevChunk == null) { + prevChunk = createTimelineChunk(prevChunkEntity) + } + prevChunk?.loadMore(offsetCount, direction, fetchFromServerIfNeeded) ?: LoadMoreResult.FAILURE + } + fetchFromServerIfNeeded -> { + fetchFromServer(offsetCount, chunkEntity.prevToken, direction) + } + else -> { + LoadMoreResult.SUCCESS } - prevChunk?.loadMore(offsetCount, direction) ?: LoadMoreResult.FAILURE } } } @@ -239,7 +258,9 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, prevChunk?.close(closeNext = false, closePrev = true) } nextChunk = null + nextChunkLatch?.cancel() prevChunk = null + prevChunkLatch?.cancel() chunkEntity.removeChangeListener(chunkObjectListener) timelineEventEntities.removeChangeListener(timelineEventCollectionListener) } @@ -247,7 +268,7 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, /** * This method tries to read events from the current chunk. */ - private suspend fun loadFromDb(count: Long, direction: Timeline.Direction): Long { + private suspend fun loadFromStorage(count: Int, direction: Timeline.Direction): Int { val displayIndex = getNextDisplayIndex(direction) ?: return 0 val baseQuery = timelineEventEntities.where() val timelineEvents = baseQuery.offsets(direction, count, displayIndex).findAll().orEmpty() @@ -259,6 +280,9 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, timelineEvents .mapIndexed { index, timelineEventEntity -> val timelineEvent = timelineEventEntity.buildAndDecryptIfNeeded() + if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) { + isLastBackward.set(true) + } if (direction == Timeline.Direction.FORWARDS) { builtEventsIndexes[timelineEvent.eventId] = index builtEvents.add(index, timelineEvent) @@ -268,7 +292,7 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, } } onBuiltEvents() - return timelineEvents.size.toLong() + return timelineEvents.size } /** @@ -309,23 +333,35 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, * It will take care to update the database by inserting new events and linking new chunk * with this one. */ - private suspend fun fetchFromServer(token: String?, direction: Timeline.Direction): LoadMoreResult { - val paginationResult = try { + private suspend fun fetchFromServer(count: Int, token: String?, direction: Timeline.Direction): LoadMoreResult { + val latch = if (direction == Timeline.Direction.FORWARDS) { + nextChunkLatch = CompletableDeferred() + nextChunkLatch + } else { + prevChunkLatch = CompletableDeferred() + prevChunkLatch + } + val loadMoreResult = try { if (token == null) { if (direction == Timeline.Direction.BACKWARDS || !chunkEntity.hasBeenALastForwardChunk()) return LoadMoreResult.REACHED_END val lastKnownEventId = chunkEntity.sortedTimelineEvents().firstOrNull()?.eventId ?: return LoadMoreResult.FAILURE - val taskParams = FetchTokenAndPaginateTask.Params(roomId, lastKnownEventId, direction.toPaginationDirection(), PAGINATION_COUNT) - fetchTokenAndPaginateTask.execute(taskParams) + val taskParams = FetchTokenAndPaginateTask.Params(roomId, lastKnownEventId, direction.toPaginationDirection(), count) + fetchTokenAndPaginateTask.execute(taskParams).toLoadMoreResult() } else { - Timber.v("Fetch more events on server") - val taskParams = PaginationTask.Params(roomId, token, direction.toPaginationDirection(), PAGINATION_COUNT) - paginationTask.execute(taskParams) + Timber.v("Fetch $count more events on server") + val taskParams = PaginationTask.Params(roomId, token, direction.toPaginationDirection(), count) + paginationTask.execute(taskParams).toLoadMoreResult() } } catch (failure: Throwable) { Timber.e("Failed to fetch from server: $failure", failure) - return LoadMoreResult.FAILURE + LoadMoreResult.FAILURE + } + return if (loadMoreResult == LoadMoreResult.SUCCESS) { + latch?.await() + loadMore(count, direction, fetchOnServerIfNeeded = false) + } else { + loadMoreResult } - return paginationResult.toLoadMoreResult() } private fun TokenChunkEventPersistor.Result.toLoadMoreResult(): LoadMoreResult { @@ -358,6 +394,9 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, .map { it.buildAndDecryptIfNeeded() } builtEventsIndexes.entries.filter { it.value >= range.startIndex }.forEach { it.setValue(it.value + range.length) } newItems.mapIndexed { index, timelineEvent -> + if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) { + isLastBackward.set(true) + } val correctedIndex = range.startIndex + index builtEvents.add(correctedIndex, timelineEvent) builtEventsIndexes[timelineEvent.eventId] = correctedIndex @@ -421,7 +460,7 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, private fun RealmQuery.offsets( direction: Timeline.Direction, - count: Long, + count: Int, startDisplayIndex: Int ): RealmQuery { sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) @@ -430,7 +469,7 @@ private fun RealmQuery.offsets( } else { greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex) } - return limit(count) + return limit(count.toLong()) } private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index ac6de2ebb0..2fd2f4cf87 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -174,7 +174,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private var inSubmitList: Boolean = false private var hasReachedInvite: Boolean = false private var hasUTD: Boolean = false - private var hasReachedCreateEvent: Boolean = false private var positionOfReadMarker: Int? = null private var partialState: PartialState = PartialState() @@ -287,7 +286,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec return } // Avoid displaying two loaders if there is no elements between them - val showBackwardsLoader = (!showingForwardLoader || timelineModels.isNotEmpty()) && !hasReachedCreateEvent + val showBackwardsLoader = !showingForwardLoader || timelineModels.isNotEmpty() // We can hide the loader but still add the item to controller so it can trigger backwards pagination LoadingItem_() .id("backward_loading_item_$timestamp") @@ -302,6 +301,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec submitSnapshot(snapshot) } + override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) { + requestDelayedModelBuild(0) + } + private fun submitSnapshot(newSnapshot: List) { backgroundHandler.post { inSubmitList = true @@ -456,14 +459,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private fun preprocessReverseEvents() { receiptsByEvent.clear() timelineEventsGroups.clear() - hasReachedCreateEvent = false val itr = currentSnapshot.listIterator(currentSnapshot.size) var lastShownEventId: String? = null while (itr.hasPrevious()) { val event = itr.previous() - if (!hasReachedCreateEvent && event.root.type == EventType.STATE_ROOM_CREATE) { - hasReachedCreateEvent = true - } timelineEventsGroups.addOrIgnore(event) val currentReadReceipts = ArrayList(event.readReceipts).filter { it.user.userId != session.myUserId From b53433e61bc455ed2e7ee19c699eed9428a2871b Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 8 Dec 2021 11:07:07 +0100 Subject: [PATCH 23/29] Timeline: some clean up --- .../session/room/timeline/DefaultTimeline.kt | 9 +++--- .../room/timeline/LoadTimelineStrategy.kt | 6 +--- .../session/room/timeline/TimelineChunk.kt | 29 +++++++++---------- 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 5e9efecc67..a2398bd750 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -74,18 +74,17 @@ internal class DefaultTimeline internal constructor(private val roomId: String, private val sequencer = SemaphoreCoroutineSequencer() private val strategyDependencies = LoadTimelineStrategy.Dependencies( - timelineScope = timelineScope, - eventDecryptor = eventDecryptor, timelineSettings = settings, + realm = backgroundRealm, + eventDecryptor = eventDecryptor, paginationTask = paginationTask, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, + getContextOfEventTask = getEventTask, timelineInput = timelineInput, timelineEventMapper = timelineEventMapper, - realm = backgroundRealm, - getContextOfEventTask = getEventTask, threadsAwarenessHandler = threadsAwarenessHandler, - onLimitedTimeline = this::onLimitedTimeline, onEventsUpdated = this::postSnapshot, + onLimitedTimeline = this::onLimitedTimeline, onNewTimelineEvents = this::onNewTimelineEvents ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index fb0971ad47..d51c2c28cb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -21,8 +21,6 @@ import io.realm.OrderedRealmCollectionChangeListener import io.realm.Realm import io.realm.RealmResults import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline @@ -65,7 +63,6 @@ internal class LoadTimelineStrategy( data class Dependencies( val timelineSettings: TimelineSettings, - val timelineScope: CoroutineScope, val realm: AtomicReference, val eventDecryptor: TimelineEventDecryptor, val paginationTask: PaginationTask, @@ -228,8 +225,7 @@ internal class LoadTimelineStrategy( uiEchoManager = uiEchoManager, threadsAwarenessHandler = dependencies.threadsAwarenessHandler, initialEventId = mode.originEventId(), - onBuiltEvents = dependencies.onEventsUpdated, - timelineScope = dependencies.timelineScope + onBuiltEvents = dependencies.onEventsUpdated ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index 78bc02f9c7..9686e49987 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -23,7 +23,6 @@ import io.realm.RealmQuery import io.realm.RealmResults import io.realm.Sort import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.EventType @@ -46,19 +45,18 @@ import java.util.concurrent.atomic.AtomicBoolean * It does mainly listen to the db timeline events. * It also triggers pagination to the server when needed, or dispatch to the prev or next chunk if any. */ -internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, - private val timelineScope: CoroutineScope, - private val timelineSettings: TimelineSettings, - private val roomId: String, - private val timelineId: String, - private val eventDecryptor: TimelineEventDecryptor, - private val paginationTask: PaginationTask, - private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, - private val timelineEventMapper: TimelineEventMapper, - private val uiEchoManager: UIEchoManager? = null, - private val threadsAwarenessHandler: ThreadsAwarenessHandler, - private val initialEventId: String?, - private val onBuiltEvents: () -> Unit) { +internal class TimelineChunk(private val chunkEntity: ChunkEntity, + private val timelineSettings: TimelineSettings, + private val roomId: String, + private val timelineId: String, + private val eventDecryptor: TimelineEventDecryptor, + private val paginationTask: PaginationTask, + private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, + private val timelineEventMapper: TimelineEventMapper, + private val uiEchoManager: UIEchoManager? = null, + private val threadsAwarenessHandler: ThreadsAwarenessHandler, + private val initialEventId: String?, + private val onBuiltEvents: () -> Unit) { private val isLastForward = AtomicBoolean(chunkEntity.isLastForward) private val isLastBackward = AtomicBoolean(chunkEntity.isLastBackward) @@ -442,11 +440,10 @@ internal class TimelineChunk constructor(private val chunkEntity: ChunkEntity, if (chunkEntity == null) return null return TimelineChunk( chunkEntity = chunkEntity, - timelineScope = timelineScope, timelineSettings = timelineSettings, + roomId = roomId, timelineId = timelineId, eventDecryptor = eventDecryptor, - roomId = roomId, paginationTask = paginationTask, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, timelineEventMapper = timelineEventMapper, From 1e2e9e1070a13f852fc674460bf45d6ad54d385e Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 9 Dec 2021 12:26:40 +0100 Subject: [PATCH 24/29] Timeline: change a bit when postPagination is triggered --- .../TimelineSimpleBackPaginationTest.kt | 2 +- .../sdk/api/session/room/timeline/Timeline.kt | 3 +- .../session/room/timeline/DefaultTimeline.kt | 84 ++++++++++++++----- .../room/timeline/LoadTimelineStrategy.kt | 6 +- .../room/timeline/SendingEventsDataSource.kt | 8 +- .../session/room/timeline/TimelineChunk.kt | 12 +-- 6 files changed, 77 insertions(+), 38 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt index bec6886fb1..d87a7269f6 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt @@ -90,7 +90,7 @@ class TimelineSimpleBackPaginationTest : InstrumentedTest { assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS)) val onlySentEvents = runBlocking { - bobTimeline.awaitSnapshot() + bobTimeline.getSnapshot() } .filter { it.root.isTextMessage() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt index 443b1a8f13..241e5f3b9b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt @@ -73,7 +73,6 @@ interface Timeline { /** * This is the same than the regular paginate method but waits for the results instead * of relying on the timeline listener. - * Note that it will still trigger onTimelineUpdated internally. */ suspend fun awaitPaginate(direction: Direction, count: Int): List @@ -90,7 +89,7 @@ interface Timeline { /** * Returns a snapshot of the timeline in his current state. */ - suspend fun awaitSnapshot(): List + fun getSnapshot(): List interface Listener { /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index a2398bd750..31ed8868d1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -24,6 +24,10 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.internal.closeQuietly @@ -72,6 +76,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, private val timelineDispatcher = BACKGROUND_HANDLER.asCoroutineDispatcher() private val timelineScope = CoroutineScope(SupervisorJob() + timelineDispatcher) private val sequencer = SemaphoreCoroutineSequencer() + private val postSnapshotSignalFlow = MutableSharedFlow(0) private val strategyDependencies = LoadTimelineStrategy.Dependencies( timelineSettings = settings, @@ -83,7 +88,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, timelineInput = timelineInput, timelineEventMapper = timelineEventMapper, threadsAwarenessHandler = threadsAwarenessHandler, - onEventsUpdated = this::postSnapshot, + onEventsUpdated = this::sendSignalToPostSnapshot, onLimitedTimeline = this::onLimitedTimeline, onNewTimelineEvents = this::onNewTimelineEvents ) @@ -95,7 +100,12 @@ internal class DefaultTimeline internal constructor(private val roomId: String, override fun addListener(listener: Timeline.Listener): Boolean { listeners.add(listener) - postSnapshot() + timelineScope.launch { + val snapshot = strategy.buildSnapshot() + withContext(Dispatchers.Main) { + tryOrNull { listener.onTimelineUpdated(snapshot) } + } + } return true } @@ -117,7 +127,9 @@ internal class DefaultTimeline internal constructor(private val roomId: String, val realm = Realm.getInstance(realmConfiguration) ensureReadReceiptAreLoaded(realm) backgroundRealm.set(realm) + listenToPostSnapshotSignals() openAround(initialEventId) + postSnapshot() } } } @@ -148,7 +160,10 @@ internal class DefaultTimeline internal constructor(private val roomId: String, override fun paginate(direction: Timeline.Direction, count: Int) { timelineScope.launch { - loadMore(count, direction, fetchOnServerIfNeeded = true) + val postSnapshot = loadMore(count, direction, fetchOnServerIfNeeded = true) + if (postSnapshot) { + postSnapshot() + } } } @@ -156,11 +171,11 @@ internal class DefaultTimeline internal constructor(private val roomId: String, withContext(timelineDispatcher) { loadMore(count, direction, fetchOnServerIfNeeded = true) } - return awaitSnapshot() + return getSnapshot() } - override suspend fun awaitSnapshot(): List = withContext(timelineDispatcher) { - strategy.buildSnapshot() + override fun getSnapshot(): List { + return strategy.buildSnapshot() } override fun getIndexOfEvent(eventId: String?): Int? { @@ -176,7 +191,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, }.get() } - private suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean) { + private suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean): Boolean { val baseLogMessage = "loadMore(count: $count, direction: $direction, roomId: $roomId, fetchOnServer: $fetchOnServerIfNeeded)" Timber.v("$baseLogMessage started") if (!isStarted.get()) { @@ -185,11 +200,11 @@ internal class DefaultTimeline internal constructor(private val roomId: String, val currentState = getPaginationState(direction) if (!currentState.hasMoreToLoad) { Timber.v("$baseLogMessage : nothing more to load") - return + return false } if (currentState.loading) { Timber.v("$baseLogMessage : already loading") - return + return false } updateState(direction) { it.copy(loading = true) @@ -200,6 +215,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, updateState(direction) { it.copy(loading = false, hasMoreToLoad = hasMoreToLoad) } + return true } private suspend fun openAround(eventId: String?) = withContext(timelineDispatcher) { @@ -221,9 +237,10 @@ internal class DefaultTimeline internal constructor(private val roomId: String, direction = Timeline.Direction.BACKWARDS, fetchOnServerIfNeeded = false ) + Timber.v("$baseLogMessage finished") } - private suspend fun initPaginationStates(eventId: String?) { + private fun initPaginationStates(eventId: String?) { updateState(Timeline.Direction.FORWARDS) { it.copy(loading = false, hasMoreToLoad = eventId != null) } @@ -232,21 +249,40 @@ internal class DefaultTimeline internal constructor(private val roomId: String, } } + private fun sendSignalToPostSnapshot(withThrottling: Boolean) { + timelineScope.launch { + if (withThrottling) { + postSnapshotSignalFlow.emit(Unit) + } else { + postSnapshot() + } + } + } + + @Suppress("EXPERIMENTAL_API_USAGE") + private fun listenToPostSnapshotSignals() { + postSnapshotSignalFlow + .sample(150) + .onEach { + postSnapshot() + } + .launchIn(timelineScope) + } + private fun onLimitedTimeline() { timelineScope.launch { initPaginationStates(null) loadMore(settings.initialSize, Timeline.Direction.BACKWARDS, false) + postSnapshot() } } - private fun postSnapshot() { - timelineScope.launch { - val snapshot = strategy.buildSnapshot() - Timber.v("Post snapshot of ${snapshot.size} items") - withContext(Dispatchers.Main) { - listeners.forEach { - tryOrNull { it.onTimelineUpdated(snapshot) } - } + private suspend fun postSnapshot() { + val snapshot = strategy.buildSnapshot() + Timber.v("Post snapshot of ${snapshot.size} events") + withContext(Dispatchers.Main) { + listeners.forEach { + tryOrNull { it.onTimelineUpdated(snapshot) } } } } @@ -259,7 +295,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, } } - private suspend fun updateState(direction: Timeline.Direction, update: (Timeline.PaginationState) -> Timeline.PaginationState) { + private fun updateState(direction: Timeline.Direction, update: (Timeline.PaginationState) -> Timeline.PaginationState) { val stateReference = when (direction) { Timeline.Direction.FORWARDS -> forwardState Timeline.Direction.BACKWARDS -> backwardState @@ -272,10 +308,12 @@ internal class DefaultTimeline internal constructor(private val roomId: String, } } - private suspend fun postPaginationState(direction: Timeline.Direction, state: Timeline.PaginationState) = withContext(Dispatchers.Main) { - Timber.v("Post $direction pagination state: $state ") - listeners.forEach { - tryOrNull { it.onStateUpdated(direction, state) } + private fun postPaginationState(direction: Timeline.Direction, state: Timeline.PaginationState) { + timelineScope.launch(Dispatchers.Main) { + Timber.v("Post $direction pagination state: $state ") + listeners.forEach { + tryOrNull { it.onStateUpdated(direction, state) } + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index d51c2c28cb..0111996345 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -71,7 +71,7 @@ internal class LoadTimelineStrategy( val timelineInput: TimelineInput, val timelineEventMapper: TimelineEventMapper, val threadsAwarenessHandler: ThreadsAwarenessHandler, - val onEventsUpdated: () -> Unit, + val onEventsUpdated: (Boolean) -> Unit, val onLimitedTimeline: () -> Unit, val onNewTimelineEvents: (List) -> Unit ) @@ -110,7 +110,7 @@ internal class LoadTimelineStrategy( } if (uiEchoManager.onLocalEchoCreated(timelineEvent)) { dependencies.onNewTimelineEvents(listOf(timelineEvent.eventId)) - dependencies.onEventsUpdated() + dependencies.onEventsUpdated(false) } } @@ -119,7 +119,7 @@ internal class LoadTimelineStrategy( return } if (uiEchoManager.onSendStateUpdated(eventId, sendState)) { - dependencies.onEventsUpdated() + dependencies.onEventsUpdated(false) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt index c6d10a23fb..a98de1c595 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt @@ -37,15 +37,17 @@ internal class RealmSendingEventsDataSource( private val realm: AtomicReference, private val uiEchoManager: UIEchoManager, private val timelineEventMapper: TimelineEventMapper, - private val onEventsUpdated: () -> Unit + private val onEventsUpdated: (Boolean) -> Unit ) : SendingEventsDataSource { private var roomEntity: RoomEntity? = null private var sendingTimelineEvents: RealmList? = null + private var frozenSendingTimelineEvents: RealmList? = null private val sendingTimelineEventsListener = RealmChangeListener> { events -> uiEchoManager.onSentEventsInDatabase(events.map { it.eventId }) - onEventsUpdated() + frozenSendingTimelineEvents = sendingTimelineEvents?.freeze() + onEventsUpdated(false) } override fun start() { @@ -65,7 +67,7 @@ internal class RealmSendingEventsDataSource( val builtSendingEvents = mutableListOf() uiEchoManager.getInMemorySendingEvents() .addWithUiEcho(builtSendingEvents) - sendingTimelineEvents?.freeze() + frozenSendingTimelineEvents ?.filter { timelineEvent -> builtSendingEvents.none { it.eventId == timelineEvent.eventId } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index 9686e49987..b3a8808ba7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -56,7 +56,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, private val uiEchoManager: UIEchoManager? = null, private val threadsAwarenessHandler: ThreadsAwarenessHandler, private val initialEventId: String?, - private val onBuiltEvents: () -> Unit) { + private val onBuiltEvents: (Boolean) -> Unit) { private val isLastForward = AtomicBoolean(chunkEntity.isLastForward) private val isLastBackward = AtomicBoolean(chunkEntity.isLastBackward) @@ -86,6 +86,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, } private val timelineEventCollectionListener = OrderedRealmCollectionChangeListener { results: RealmResults, changeSet: OrderedCollectionChangeSet -> + Timber.v("on timeline events chunk update") val frozenResults = results.freeze() handleDatabaseChangeSet(frozenResults, changeSet) } @@ -135,9 +136,9 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, } else if (direction == Timeline.Direction.BACKWARDS && prevChunk != null) { return prevChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE } - val loadFromDbCount = loadFromStorage(count, direction) - Timber.v("Has loaded $loadFromDbCount items from storage") - val offsetCount = count - loadFromDbCount + val loadFromStorageCount = loadFromStorage(count, direction) + Timber.v("Has loaded $loadFromStorageCount items from storage in $direction") + val offsetCount = count - loadFromStorageCount return if (direction == Timeline.Direction.FORWARDS && isLastForward.get()) { LoadMoreResult.REACHED_END } else if (direction == Timeline.Direction.BACKWARDS && isLastBackward.get()) { @@ -289,7 +290,6 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, builtEvents.add(timelineEvent) } } - onBuiltEvents() return timelineEvents.size } @@ -412,7 +412,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, } } if (insertions.isNotEmpty() || modifications.isNotEmpty()) { - onBuiltEvents() + onBuiltEvents(true) } } From c830d4992437158c71b8f2340f54066093c1dead Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 9 Dec 2021 12:27:36 +0100 Subject: [PATCH 25/29] Timeline: remove onStateUpdated in controller --- .../home/room/detail/timeline/TimelineEventController.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 2fd2f4cf87..241ccb7428 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -301,10 +301,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec submitSnapshot(snapshot) } - override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) { - requestDelayedModelBuild(0) - } - private fun submitSnapshot(newSnapshot: List) { backgroundHandler.post { inSubmitList = true From 31ba912d6e021c86d7638bb51b80b1021ee9bb04 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 9 Dec 2021 12:28:13 +0100 Subject: [PATCH 26/29] Timeline: url preview listen to Flow in ViewModel --- .../home/room/detail/RoomDetailViewModel.kt | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 18acc4ff14..3ac3ed5ed4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -160,6 +160,7 @@ class RoomDetailViewModel @AssistedInject constructor( observeMyRoomMember() observeActiveRoomWidgets() observePowerLevel() + setupPreviewUrlObservers() room.getRoomSummaryLive() viewModelScope.launch(Dispatchers.IO) { tryOrNull { room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) } @@ -263,6 +264,30 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun setupPreviewUrlObservers() { + if (!vectorPreferences.showUrlPreviews()) { + return + } + combine( + timelineEvents, + room.flow().liveRoomSummary() + .unwrap() + .map { it.isEncrypted } + .distinctUntilChanged() + ) { snapshot, isRoomEncrypted -> + if (isRoomEncrypted) { + return@combine + } + withContext(Dispatchers.Default) { + Timber.v("On new timeline events for urlpreview on ${Thread.currentThread()}") + snapshot.forEach { + previewUrlRetriever.getPreviewUrl(it) + } + } + } + .launchIn(viewModelScope) + } + fun getOtherUserIds() = room.roomSummary()?.otherMemberIds override fun handle(action: RoomDetailAction) { @@ -1031,16 +1056,6 @@ class RoomDetailViewModel @AssistedInject constructor( // tryEmit doesn't work with SharedFlow without cache timelineEvents.emit(snapshot) } - // PreviewUrl - if (vectorPreferences.showUrlPreviews()) { - withState { state -> - snapshot - .takeIf { state.asyncRoomSummary.invoke()?.isEncrypted == false } - ?.forEach { - previewUrlRetriever.getPreviewUrl(it) - } - } - } } override fun onTimelineFailure(throwable: Throwable) { From faebf95e1c2fc81fcbea0ca37733877759c07765 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 9 Dec 2021 12:28:53 +0100 Subject: [PATCH 27/29] Timeline: remove LifecycleOwner on EpoxyModel as it's not used (and takes some time uselessly) --- .../main/java/im/vector/app/core/epoxy/VectorEpoxyModel.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/epoxy/VectorEpoxyModel.kt b/vector/src/main/java/im/vector/app/core/epoxy/VectorEpoxyModel.kt index fcb5a473a4..e9dd72a6f2 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/VectorEpoxyModel.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/VectorEpoxyModel.kt @@ -30,24 +30,19 @@ import kotlinx.coroutines.cancelChildren /** * EpoxyModelWithHolder which can listen to visibility state change */ -abstract class VectorEpoxyModel : EpoxyModelWithHolder(), LifecycleOwner { +abstract class VectorEpoxyModel : EpoxyModelWithHolder(){ protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) - private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) - - override fun getLifecycle() = lifecycleRegistry private var onModelVisibilityStateChangedListener: OnVisibilityStateChangedListener? = null @CallSuper override fun bind(holder: H) { super.bind(holder) - lifecycleRegistry.currentState = Lifecycle.State.STARTED } @CallSuper override fun unbind(holder: H) { - lifecycleRegistry.currentState = Lifecycle.State.DESTROYED coroutineScope.coroutineContext.cancelChildren() super.unbind(holder) } From bf287d1827beb60f65b25668f404012b8676f20d Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 9 Dec 2021 12:33:56 +0100 Subject: [PATCH 28/29] Timeline: clean up --- .../TimelineSimpleBackPaginationTest.kt | 5 ++--- .../sdk/internal/database/DatabaseCleaner.kt | 2 +- .../database/RealmSessionStoreMigration.kt | 1 - .../database/helper/ChunkEntityHelper.kt | 6 +++--- .../internal/database/model/ChunkEntity.kt | 2 +- .../internal/database/query/ReadQueries.kt | 1 - .../session/room/read/DefaultReadService.kt | 1 - .../session/room/timeline/DefaultTimeline.kt | 11 ++++++----- .../room/timeline/DefaultTimelineService.kt | 5 ++++- .../room/timeline/LoadTimelineStrategy.kt | 2 -- .../session/room/timeline/TimelineChunk.kt | 19 ++++++++++--------- .../room/timeline/TokenChunkEventPersistor.kt | 2 +- .../session/room/timeline/UIEchoManager.kt | 1 - .../vector/app/core/epoxy/VectorEpoxyModel.kt | 5 +---- 14 files changed, 29 insertions(+), 34 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt index d87a7269f6..b75df9b5a2 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt @@ -43,7 +43,6 @@ class TimelineSimpleBackPaginationTest : InstrumentedTest { @Test fun timeline_backPaginate_shouldReachEndOfTimeline() { - val numberOfMessagesToSent = 200 val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false) @@ -69,12 +68,12 @@ class TimelineSimpleBackPaginationTest : InstrumentedTest { bobTimeline.start() commonTestHelper.waitWithLatch(timeout = TestConstants.timeOutMillis * 10) { - val listener = object : Timeline.Listener { override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) { - if (direction == Timeline.Direction.FORWARDS) + if (direction == Timeline.Direction.FORWARDS) { return + } if (state.hasMoreToLoad && !state.loading) { bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) } else if (!state.hasMoreToLoad) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt index 09fbb2bfa0..6d567600ad 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt @@ -44,7 +44,7 @@ internal class DatabaseCleaner @Inject constructor(@SessionDatabase private val awaitTransaction(realmConfiguration) { realm -> val allRooms = realm.where(RoomEntity::class.java).findAll() Timber.v("There are ${allRooms.size} rooms in this session") - //cleanUp(realm, MAX_NUMBER_OF_EVENTS_IN_DB / 2L) + // cleanUp(realm, MAX_NUMBER_OF_EVENTS_IN_DB / 2L) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 9934c2570b..508af250c2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -414,5 +414,4 @@ internal class RealmSessionStoreMigration @Inject constructor( } } } - } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt index 136734c02e..c21bf74d93 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt @@ -110,7 +110,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String, true } } - //numberOfTimelineEvents++ + // numberOfTimelineEvents++ timelineEvents.add(timelineEventEntity) } @@ -207,11 +207,11 @@ internal fun ChunkEntity.isMoreRecentThan(chunkToCheck: ChunkEntity): Boolean { if (this.isLastForward) return true if (chunkToCheck.isLastForward) return false // Check if the chunk to check is linked to this one - if(chunkToCheck.doesNextChunksVerifyCondition { it == this }){ + if (chunkToCheck.doesNextChunksVerifyCondition { it == this }) { return true } // Otherwise check if this chunk is linked to last forward - if(this.doesNextChunksVerifyCondition { it.isLastForward }){ + if (this.doesNextChunksVerifyCondition { it.isLastForward }) { return true } // We don't know, so we assume it's false diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt index 8b301d15fa..82b7517181 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt @@ -31,7 +31,7 @@ internal open class ChunkEntity(@Index var prevToken: String? = null, var nextChunk: ChunkEntity? = null, var stateEvents: RealmList = RealmList(), var timelineEvents: RealmList = RealmList(), - //var numberOfTimelineEvents: Long = 0, + // var numberOfTimelineEvents: Long = 0, // Only one chunk will have isLastForward == true @Index var isLastForward: Boolean = false, @Index var isLastBackward: Boolean = false diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt index c8879b009e..c9c96b9cc1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt @@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.database.query import io.realm.Realm import io.realm.RealmConfiguration import org.matrix.android.sdk.api.session.events.model.LocalEcho -import org.matrix.android.sdk.internal.database.helper.doesNextChunksVerifyCondition import org.matrix.android.sdk.internal.database.helper.isMoreRecentThan import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt index 7e42e4f9c2..b30c66c82e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt @@ -34,7 +34,6 @@ import org.matrix.android.sdk.internal.database.query.isEventRead import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId -import org.matrix.android.sdk.internal.task.TaskExecutor internal class DefaultReadService @AssistedInject constructor( @Assisted private val roomId: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 31ed8868d1..bb15bcb9ca 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.room.timeline import io.realm.Realm import io.realm.RealmConfiguration import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.cancelChildren @@ -31,6 +30,7 @@ import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.internal.closeQuietly +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -53,6 +53,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, private val loadRoomMembersTask: LoadRoomMembersTask, private val readReceiptHandler: ReadReceiptHandler, private val settings: TimelineSettings, + private val coroutineDispatchers: MatrixCoroutineDispatchers, paginationTask: PaginationTask, getEventTask: GetContextOfEventTask, fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, @@ -102,7 +103,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, listeners.add(listener) timelineScope.launch { val snapshot = strategy.buildSnapshot() - withContext(Dispatchers.Main) { + withContext(coroutineDispatchers.main) { tryOrNull { listener.onTimelineUpdated(snapshot) } } } @@ -280,7 +281,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, private suspend fun postSnapshot() { val snapshot = strategy.buildSnapshot() Timber.v("Post snapshot of ${snapshot.size} events") - withContext(Dispatchers.Main) { + withContext(coroutineDispatchers.main) { listeners.forEach { tryOrNull { it.onTimelineUpdated(snapshot) } } @@ -288,7 +289,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, } private fun onNewTimelineEvents(eventIds: List) { - timelineScope.launch(Dispatchers.Main) { + timelineScope.launch(coroutineDispatchers.main) { listeners.forEach { tryOrNull { it.onNewTimelineEvents(eventIds) } } @@ -309,7 +310,7 @@ internal class DefaultTimeline internal constructor(private val roomId: String, } private fun postPaginationState(direction: Timeline.Direction, state: Timeline.PaginationState) { - timelineScope.launch(Dispatchers.Main) { + timelineScope.launch(coroutineDispatchers.main) { Timber.v("Post $direction pagination state: $state ") listeners.forEach { tryOrNull { it.onStateUpdated(direction, state) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index ab3f574130..126374b430 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -23,6 +23,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.realm.Sort import io.realm.kotlin.where +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.session.events.model.isImageMessage import org.matrix.android.sdk.api.session.events.model.isVideoMessage import org.matrix.android.sdk.api.session.room.timeline.Timeline @@ -54,7 +55,8 @@ internal class DefaultTimelineService @AssistedInject constructor( private val timelineEventMapper: TimelineEventMapper, private val loadRoomMembersTask: LoadRoomMembersTask, private val threadsAwarenessHandler: ThreadsAwarenessHandler, - private val readReceiptHandler: ReadReceiptHandler + private val readReceiptHandler: ReadReceiptHandler, + private val coroutineDispatchers: MatrixCoroutineDispatchers ) : TimelineService { @AssistedFactory @@ -68,6 +70,7 @@ internal class DefaultTimelineService @AssistedInject constructor( initialEventId = eventId, settings = settings, realmConfiguration = monarchy.realmConfiguration, + coroutineDispatchers = coroutineDispatchers, paginationTask = paginationTask, timelineEventMapper = timelineEventMapper, timelineInput = timelineInput, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index 0111996345..2e71a8099f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -230,5 +230,3 @@ internal class LoadTimelineStrategy( } } } - - diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index b3a8808ba7..14cba2a4b8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -85,11 +85,12 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, } } - private val timelineEventCollectionListener = OrderedRealmCollectionChangeListener { results: RealmResults, changeSet: OrderedCollectionChangeSet -> - Timber.v("on timeline events chunk update") - val frozenResults = results.freeze() - handleDatabaseChangeSet(frozenResults, changeSet) - } + private val timelineEventsChangeListener = + OrderedRealmCollectionChangeListener { results: RealmResults, changeSet: OrderedCollectionChangeSet -> + Timber.v("on timeline events chunk update") + val frozenResults = results.freeze() + handleDatabaseChangeSet(frozenResults, changeSet) + } private var timelineEventEntities: RealmResults = chunkEntity.sortedTimelineEvents() private val builtEvents: MutableList = Collections.synchronizedList(ArrayList()) @@ -99,7 +100,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, private var prevChunk: TimelineChunk? = null init { - timelineEventEntities.addChangeListener(timelineEventCollectionListener) + timelineEventEntities.addChangeListener(timelineEventsChangeListener) chunkEntity.addChangeListener(chunkObjectListener) } @@ -261,7 +262,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, prevChunk = null prevChunkLatch?.cancel() chunkEntity.removeChangeListener(chunkObjectListener) - timelineEventEntities.removeChangeListener(timelineEventCollectionListener) + timelineEventEntities.removeChangeListener(timelineEventsChangeListener) } /** @@ -311,8 +312,8 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity, val timelineEvent = buildTimelineEvent(this) val transactionId = timelineEvent.root.unsignedData?.transactionId uiEchoManager?.onSyncedEvent(transactionId) - if (timelineEvent.isEncrypted() - && timelineEvent.root.mxDecryptionResult == null) { + if (timelineEvent.isEncrypted() && + timelineEvent.root.mxDecryptionResult == null) { timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) } } return timelineEvent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt index 54d7412166..4625155c0a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -133,7 +133,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri if (event.eventId == null || event.senderId == null) { return@forEach } - //We check for the timeline event with this id + // We check for the timeline event with this id val eventId = event.eventId val existingTimelineEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst() // If it exists, we want to stop here, just link the prevChunk diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt index 5b2cf001f3..16d36c0cd9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt @@ -24,7 +24,6 @@ import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import timber.log.Timber import java.util.Collections diff --git a/vector/src/main/java/im/vector/app/core/epoxy/VectorEpoxyModel.kt b/vector/src/main/java/im/vector/app/core/epoxy/VectorEpoxyModel.kt index e9dd72a6f2..6142748bf4 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/VectorEpoxyModel.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/VectorEpoxyModel.kt @@ -17,9 +17,6 @@ package im.vector.app.core.epoxy import androidx.annotation.CallSuper -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LifecycleRegistry import com.airbnb.epoxy.EpoxyModelWithHolder import com.airbnb.epoxy.VisibilityState import kotlinx.coroutines.CoroutineScope @@ -30,7 +27,7 @@ import kotlinx.coroutines.cancelChildren /** * EpoxyModelWithHolder which can listen to visibility state change */ -abstract class VectorEpoxyModel : EpoxyModelWithHolder(){ +abstract class VectorEpoxyModel : EpoxyModelWithHolder() { protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) From 12b775c26dba0838a7b70de52e80f8eef206d5b6 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 3 Jan 2022 16:06:48 +0100 Subject: [PATCH 29/29] Timeline : clean after PR reviews --- changelog.d/4405.feature | 1 + changelog.d/4405.removal | 1 + .../sdk/internal/database/DatabaseCleaner.kt | 83 ------------------- .../internal/database/model/ChunkEntity.kt | 1 - .../sdk/internal/session/SessionModule.kt | 5 -- .../session/room/timeline/DefaultTimeline.kt | 28 +++---- .../room/timeline/LoadTimelineStrategy.kt | 6 +- 7 files changed, 19 insertions(+), 106 deletions(-) create mode 100644 changelog.d/4405.feature create mode 100644 changelog.d/4405.removal delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt diff --git a/changelog.d/4405.feature b/changelog.d/4405.feature new file mode 100644 index 0000000000..9a840a9d12 --- /dev/null +++ b/changelog.d/4405.feature @@ -0,0 +1 @@ +Change internal timeline management. \ No newline at end of file diff --git a/changelog.d/4405.removal b/changelog.d/4405.removal new file mode 100644 index 0000000000..2d1543cb2b --- /dev/null +++ b/changelog.d/4405.removal @@ -0,0 +1 @@ +Introduce method onStateUpdated on Timeline.Callback \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt deleted file mode 100644 index 6d567600ad..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.database - -import io.realm.RealmConfiguration -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.SessionLifecycleObserver -import org.matrix.android.sdk.internal.database.model.RoomEntity -import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.task.TaskExecutor -import timber.log.Timber -import javax.inject.Inject - -private const val MAX_NUMBER_OF_EVENTS_IN_DB = 35_000L -private const val MIN_NUMBER_OF_EVENTS_BY_CHUNK = 300 - -/** - * This class makes sure to stay under a maximum number of events as it makes Realm to be unusable when listening to events - * when the database is getting too big. This will try incrementally to remove the biggest chunks until we get below the threshold. - * We make sure to still have a minimum number of events so it's not becoming unusable. - * So this won't work for users with a big number of very active rooms. - */ -internal class DatabaseCleaner @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration, - private val taskExecutor: TaskExecutor) : SessionLifecycleObserver { - - override fun onSessionStarted(session: Session) { - taskExecutor.executorScope.launch(Dispatchers.Default) { - awaitTransaction(realmConfiguration) { realm -> - val allRooms = realm.where(RoomEntity::class.java).findAll() - Timber.v("There are ${allRooms.size} rooms in this session") - // cleanUp(realm, MAX_NUMBER_OF_EVENTS_IN_DB / 2L) - } - } - } - - /* - private fun cleanUp(realm: Realm, threshold: Long) { - val numberOfEvents = realm.where(EventEntity::class.java).findAll().size - val numberOfTimelineEvents = realm.where(TimelineEventEntity::class.java).findAll().size - Timber.v("Number of events in db: $numberOfEvents | Number of timeline events in db: $numberOfTimelineEvents") - if (threshold <= MIN_NUMBER_OF_EVENTS_BY_CHUNK || numberOfTimelineEvents < MAX_NUMBER_OF_EVENTS_IN_DB) { - Timber.v("Db is low enough") - } else { - val thresholdChunks = realm.where(ChunkEntity::class.java) - .greaterThan(ChunkEntityFields.NUMBER_OF_TIMELINE_EVENTS, threshold) - .findAll() - - Timber.v("There are ${thresholdChunks.size} chunks to clean with more than $threshold events") - for (chunk in thresholdChunks) { - val maxDisplayIndex = chunk.nextDisplayIndex(PaginationDirection.FORWARDS) - val thresholdDisplayIndex = maxDisplayIndex - threshold - val eventsToRemove = chunk.timelineEvents.where().lessThan(TimelineEventEntityFields.DISPLAY_INDEX, thresholdDisplayIndex).findAll() - Timber.v("There are ${eventsToRemove.size} events to clean in chunk: ${chunk.identifier()} from room ${chunk.room?.first()?.roomId}") - //chunk.numberOfTimelineEvents = chunk.numberOfTimelineEvents - eventsToRemove.size - eventsToRemove.forEach { - val canDeleteRoot = it.root?.stateKey == null - it.deleteOnCascade(canDeleteRoot) - } - // We reset the prevToken so we will need to fetch again. - chunk.prevToken = null - } - cleanUp(realm, (threshold / 1.5).toLong()) - } - } - - */ -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt index 82b7517181..ecb602019a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt @@ -31,7 +31,6 @@ internal open class ChunkEntity(@Index var prevToken: String? = null, var nextChunk: ChunkEntity? = null, var stateEvents: RealmList = RealmList(), var timelineEvents: RealmList = RealmList(), - // var numberOfTimelineEvents: Long = 0, // Only one chunk will have isLastForward == true @Index var isLastForward: Boolean = false, @Index var isLastBackward: Boolean = false diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index ebc2176a13..e2cfea479d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -47,7 +47,6 @@ import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorage import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor -import org.matrix.android.sdk.internal.database.DatabaseCleaner import org.matrix.android.sdk.internal.database.EventInsertLiveObserver import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory @@ -339,10 +338,6 @@ internal abstract class SessionModule { @IntoSet abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver - @Binds - @IntoSet - abstract fun bindDatabaseCleaner(cleaner: DatabaseCleaner): SessionLifecycleObserver - @Binds @IntoSet abstract fun bindRealmSessionProvider(provider: RealmSessionProvider): SessionLifecycleObserver diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index bb15bcb9ca..71823cd458 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -47,20 +47,20 @@ import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference -internal class DefaultTimeline internal constructor(private val roomId: String, - private val initialEventId: String?, - private val realmConfiguration: RealmConfiguration, - private val loadRoomMembersTask: LoadRoomMembersTask, - private val readReceiptHandler: ReadReceiptHandler, - private val settings: TimelineSettings, - private val coroutineDispatchers: MatrixCoroutineDispatchers, - paginationTask: PaginationTask, - getEventTask: GetContextOfEventTask, - fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, - timelineEventMapper: TimelineEventMapper, - timelineInput: TimelineInput, - threadsAwarenessHandler: ThreadsAwarenessHandler, - eventDecryptor: TimelineEventDecryptor) : Timeline { +internal class DefaultTimeline(private val roomId: String, + private val initialEventId: String?, + private val realmConfiguration: RealmConfiguration, + private val loadRoomMembersTask: LoadRoomMembersTask, + private val readReceiptHandler: ReadReceiptHandler, + private val settings: TimelineSettings, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + paginationTask: PaginationTask, + getEventTask: GetContextOfEventTask, + fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, + timelineEventMapper: TimelineEventMapper, + timelineInput: TimelineInput, + threadsAwarenessHandler: ThreadsAwarenessHandler, + eventDecryptor: TimelineEventDecryptor) : Timeline { companion object { val BACKGROUND_HANDLER = createBackgroundHandler("DefaultTimeline_Thread") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index 2e71a8099f..528b564e8b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -48,9 +48,9 @@ internal class LoadTimelineStrategy( private val mode: Mode, private val dependencies: Dependencies) { - sealed class Mode { - object Live : Mode() - data class Permalink(val originEventId: String) : Mode() + sealed interface Mode { + object Live : Mode + data class Permalink(val originEventId: String) : Mode fun originEventId(): String? { return if (this is Permalink) {