diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 4653d4b5b8..30ff7df4fa 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -28,6 +28,7 @@ android { targetSdkVersion 28 versionCode 1 versionName "1.0" + multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -104,17 +105,17 @@ dependencies { testImplementation 'org.robolectric:shadows-support-v4:3.0' testImplementation "io.mockk:mockk:1.8.13.kotlin13" testImplementation 'org.amshove.kluent:kluent-android:1.44' - testImplementation "androidx.arch.core:core-testing:$lifecycle_version" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" androidTestImplementation "org.koin:koin-test:$koin_version" + androidTestImplementation 'androidx.test:core:1.1.0' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test:rules:1.1.1' + androidTestImplementation 'androidx.test.ext:junit:1.1.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' androidTestImplementation 'org.amshove.kluent:kluent-android:1.44' androidTestImplementation "io.mockk:mockk-android:1.8.13.kotlin13" androidTestImplementation "androidx.arch.core:core-testing:$lifecycle_version" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" - } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt index 397dcfc89a..b76014d18b 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt @@ -16,10 +16,9 @@ package im.vector.matrix.android.session.room.timeline +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.database.helper.add import im.vector.matrix.android.internal.database.helper.addAll import im.vector.matrix.android.internal.database.helper.isUnlinked @@ -27,6 +26,9 @@ import im.vector.matrix.android.internal.database.helper.lastStateIndex import im.vector.matrix.android.internal.database.helper.merge import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection +import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents +import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeMessageEvent +import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeRoomMemberEvent import io.realm.Realm import io.realm.RealmConfiguration import io.realm.kotlin.createObject @@ -35,9 +37,10 @@ import org.amshove.kluent.shouldBeTrue import org.amshove.kluent.shouldEqual import org.junit.Before import org.junit.Test -import kotlin.random.Random +import org.junit.runner.RunWith +@RunWith(AndroidJUnit4::class) internal class ChunkEntityTest : InstrumentedTest { private lateinit var monarchy: Monarchy @@ -54,7 +57,7 @@ internal class ChunkEntityTest : InstrumentedTest { fun add_shouldAdd_whenNotAlreadyIncluded() { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeEvent(false) + val fakeEvent = createFakeMessageEvent() chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.events.size shouldEqual 1 } @@ -64,7 +67,7 @@ internal class ChunkEntityTest : InstrumentedTest { fun add_shouldNotAdd_whenAlreadyIncluded() { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeEvent(false) + val fakeEvent = createFakeMessageEvent() chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.events.size shouldEqual 1 @@ -75,7 +78,7 @@ internal class ChunkEntityTest : InstrumentedTest { fun add_shouldStateIndexIncremented_whenStateEventIsAddedForward() { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeEvent(true) + val fakeEvent = createFakeRoomMemberEvent() chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1 } @@ -85,7 +88,7 @@ internal class ChunkEntityTest : InstrumentedTest { fun add_shouldStateIndexNotIncremented_whenNoStateEventIsAdded() { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() - val fakeEvent = createFakeEvent(false) + val fakeEvent = createFakeMessageEvent() chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0 } @@ -196,15 +199,4 @@ internal class ChunkEntityTest : InstrumentedTest { } } - - private fun createFakeListOfEvents(size: Int = 10): List { - return (0 until size).map { createFakeEvent(Random.nextBoolean()) } - } - - private fun createFakeEvent(asStateEvent: Boolean = false): Event { - val eventId = Random.nextLong(System.currentTimeMillis()).toString() - val type = if (asStateEvent) EventType.STATE_ROOM_NAME else EventType.MESSAGE - return Event(type, eventId) - } - } \ No newline at end of file diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt index 35e9cb12ec..9d2edd745f 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt @@ -17,9 +17,15 @@ package im.vector.matrix.android.session.room.timeline import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toContent +import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.MyMembership +import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.model.message.MessageTextContent +import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.internal.database.helper.addAll import im.vector.matrix.android.internal.database.helper.addOrUpdate import im.vector.matrix.android.internal.database.model.ChunkEntity @@ -30,27 +36,56 @@ import kotlin.random.Random object RoomDataHelper { + private const val FAKE_TEST_SENDER = "@sender:test.org" + private val EVENT_FACTORIES = hashMapOf( + 0 to { createFakeMessageEvent() }, + 1 to { createFakeRoomMemberEvent() } + ) + fun createFakeListOfEvents(size: Int = 10): List { - return (0 until size).map { createFakeEvent(Random.nextBoolean()) } + return (0 until size).mapNotNull { + val nextInt = Random.nextInt(EVENT_FACTORIES.size) + EVENT_FACTORIES[nextInt]?.invoke() + } } - fun createFakeEvent(asStateEvent: Boolean = false): Event { - val eventId = Random.nextLong(System.currentTimeMillis()).toString() - val type = if (asStateEvent) EventType.STATE_ROOM_NAME else EventType.MESSAGE - return Event(type, eventId) + fun createFakeEvent(type: String, + content: Content? = null, + prevContent: Content? = null, + sender: String = FAKE_TEST_SENDER, + stateKey: String = FAKE_TEST_SENDER + ): Event { + return Event( + type = type, + eventId = Random.nextLong().toString(), + content = content, + prevContent = prevContent, + sender = sender, + stateKey = stateKey + ) + } + + fun createFakeMessageEvent(): Event { + val message = MessageTextContent(MessageType.MSGTYPE_TEXT, "Fake message #${Random.nextLong()}").toContent() + return createFakeEvent(EventType.MESSAGE, message) + } + + fun createFakeRoomMemberEvent(): Event { + val roomMember = RoomMember(Membership.JOIN, "Fake name #${Random.nextLong()}").toContent() + return createFakeEvent(EventType.STATE_ROOM_MEMBER, roomMember) } fun fakeInitialSync(monarchy: Monarchy, roomId: String) { monarchy.runTransactionSync { realm -> val roomEntity = realm.createObject(roomId) roomEntity.membership = MyMembership.JOINED - val eventList = createFakeListOfEvents(30) + val eventList = createFakeListOfEvents(10) val chunkEntity = realm.createObject().apply { nextToken = null prevToken = Random.nextLong(System.currentTimeMillis()).toString() isLastForward = true } - chunkEntity.addAll("roomId", eventList, PaginationDirection.FORWARDS) + chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS) roomEntity.addOrUpdate(chunkEntity) } } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineHolderTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineHolderTest.kt deleted file mode 100644 index 2063a61836..0000000000 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineHolderTest.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2019 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 im.vector.matrix.android.session.room.timeline - -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.test.annotation.UiThreadTest -import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.InstrumentedTest -import im.vector.matrix.android.LiveDataTestObserver -import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor -import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService -import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.testCoroutineDispatchers -import io.realm.Realm -import io.realm.RealmConfiguration -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -internal class TimelineHolderTest : InstrumentedTest { - - @get:Rule val testRule = InstantTaskExecutorRule() - private lateinit var monarchy: Monarchy - - @Before - fun setup() { - Realm.init(context()) - val testConfiguration = RealmConfiguration.Builder().name("test-realm").build() - Realm.deleteRealm(testConfiguration) - monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build() - } - - @Test - @UiThreadTest - fun backPaginate_shouldLoadMoreEvents_whenLoadAroundIsCalled() { - val roomId = "roomId" - val taskExecutor = TaskExecutor(testCoroutineDispatchers) - val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy) - val paginationTask = FakePaginationTask(tokenChunkEventPersistor) - val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor) - RoomDataHelper.fakeInitialSync(monarchy, roomId) - val timelineHolder = DefaultTimelineService(roomId, monarchy, taskExecutor, getContextOfEventTask, RoomMemberExtractor(roomId)) - val timelineObserver = LiveDataTestObserver.test(timelineHolder.timeline()) - timelineObserver.awaitNextValue().assertHasValue() - var timelineData = timelineObserver.value() - timelineData.events.size shouldEqual 30 - (0 until timelineData.events.size).map { - timelineData.events.loadAround(it) - } - timelineObserver.awaitNextValue().assertHasValue() - timelineData = timelineObserver.value() - timelineData.events.size shouldEqual 60 - } - - -} \ No newline at end of file diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt new file mode 100644 index 0000000000..a80e92a098 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2019 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 im.vector.matrix.android.session.room.timeline + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.session.room.timeline.Timeline +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor +import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline +import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory +import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.testCoroutineDispatchers +import io.realm.Realm +import io.realm.RealmConfiguration +import org.amshove.kluent.shouldEqual +import org.junit.Before +import org.junit.Test +import timber.log.Timber +import java.util.concurrent.CountDownLatch + +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").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(tokenChunkEventPersistor) + val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor) + val roomMemberExtractor = RoomMemberExtractor(ROOM_ID) + val timelineEventFactory = TimelineEventFactory(roomMemberExtractor) + 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 onUpdated(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 shouldEqual initialLoad + paginationCount + timeline.dispose() + } + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index 6d99c9eb50..9a66ffed62 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -35,6 +35,18 @@ inline fun Content?.toModel(): T? { } } +/** + * This methods is a facility method to map a model to a json Content + */ +@Suppress("UNCHECKED_CAST") +inline fun T?.toContent(): Content? { + return this?.let { + val moshi = MoshiProvider.providesMoshi() + val moshiAdapter = moshi.adapter(T::class.java) + return moshiAdapter.toJsonValue(it) as Content + } +} + /** * Generic event class with all possible fields for events. * The content and prevContent json fields can easily be mapped to a model with [toModel] method. diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index 9a9e4b7cfe..b92598ab76 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -54,10 +54,14 @@ internal fun ChunkEntity.merge(roomId: String, if (direction == PaginationDirection.FORWARDS) { this.nextToken = chunkToMerge.nextToken this.isLastForward = chunkToMerge.isLastForward + this.forwardsStateIndex = chunkToMerge.forwardsStateIndex + this.forwardsDisplayIndex = chunkToMerge.forwardsDisplayIndex eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) } else { this.prevToken = chunkToMerge.prevToken this.isLastBackward = chunkToMerge.isLastBackward + this.backwardsStateIndex = chunkToMerge.backwardsStateIndex + this.backwardsDisplayIndex = chunkToMerge.backwardsDisplayIndex eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) } eventsToMerge.forEach { @@ -111,8 +115,7 @@ internal fun ChunkEntity.add(roomId: String, this.displayIndex = currentDisplayIndex } // We are not using the order of the list, but will be sorting with displayIndex field - val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size - events.add(position, eventEntity) + events.add(eventEntity) } private fun ChunkEntity.assertIsManaged() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt index 7cd8bbafb9..c421b3bd3d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt @@ -48,7 +48,6 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI, return if (areAllMembersAlreadyLoaded(params.roomId)) { Try.just(true) } else { - //TODO use this token val lastToken = syncTokenStore.getLastToken() executeRequest { apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value) @@ -63,7 +62,7 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI, .tryTransactionSync { realm -> // We ignore all the already known members val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) val roomMembers = RoomMembers(realm, roomId).getLoaded() val eventsToInsert = response.roomMemberEvents.filter { !roomMembers.containsKey(it.stateKey) } @@ -78,9 +77,9 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI, private fun areAllMembersAlreadyLoaded(roomId: String): Boolean { return monarchy - .fetchAllCopiedSync { RoomEntity.where(it, roomId) } - .firstOrNull() - ?.areAllMembersLoaded ?: false + .fetchAllCopiedSync { RoomEntity.where(it, roomId) } + .firstOrNull() + ?.areAllMembersLoaded ?: false } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index ef037ac523..38555d2027 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -36,7 +36,12 @@ import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoo import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith -import io.realm.* +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 timber.log.Timber import java.util.* import java.util.concurrent.atomic.AtomicBoolean @@ -97,10 +102,14 @@ internal class DefaultTimeline( val state = getPaginationState(direction) if (state.isPaginating) { // We are getting new items from pagination - paginateInternal(startDisplayIndex, direction, state.requestedCount) + val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedCount) + if (shouldPostSnapshot) { + postSnapshot() + } } else { // We are getting new items from sync buildTimelineEvents(startDisplayIndex, direction, range.length.toLong()) + postSnapshot() } } } @@ -114,7 +123,10 @@ internal class DefaultTimeline( } Timber.v("Paginate $direction of $count items") val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex - paginateInternal(startDisplayIndex, direction, count) + val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, count) + if (shouldPostSnapshot) { + postSnapshot() + } } } @@ -191,13 +203,15 @@ internal class DefaultTimeline( /** * This has to be called on TimelineThread as it access realm live results + * @return true if snapshot should be posted */ private fun paginateInternal(startDisplayIndex: Int, direction: Timeline.Direction, - count: Int) { + count: Int): Boolean { updatePaginationState(direction) { it.copy(requestedCount = count, isPaginating = true) } val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong()) - if (builtCount < count && !hasReachedEnd(direction)) { + val shouldFetchMore = builtCount < count && !hasReachedEnd(direction) + if (shouldFetchMore) { val newRequestedCount = count - builtCount updatePaginationState(direction) { it.copy(requestedCount = newRequestedCount) } val fetchingCount = Math.max(MIN_FETCHING_COUNT, newRequestedCount) @@ -205,6 +219,7 @@ internal class DefaultTimeline( } else { updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) } } + return !shouldFetchMore } private fun snapshot(): List { @@ -252,12 +267,13 @@ internal class DefaultTimeline( } else { val count = Math.min(INITIAL_LOAD_SIZE, liveEvents.size) if (isLive) { - paginate(Timeline.Direction.BACKWARDS, count) + paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) } else { - paginate(Timeline.Direction.FORWARDS, count / 2) - paginate(Timeline.Direction.BACKWARDS, count / 2) + paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2) + paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count / 2) } } + postSnapshot() } /** @@ -266,9 +282,9 @@ internal class DefaultTimeline( private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { val token = getTokenLive(direction) ?: return val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) + from = token, + direction = direction.toPaginationDirection(), + limit = limit) Timber.v("Should fetch $limit items $direction") paginationTask.configureWith(params) @@ -336,8 +352,6 @@ internal class DefaultTimeline( builtEvents.add(position, timelineEvent) } Timber.v("Built ${offsetResults.size} items from db") - val snapshot = snapshot() - mainHandler.post { listener?.onUpdated(snapshot) } return offsetResults.size } @@ -399,6 +413,11 @@ internal class DefaultTimeline( contextOfEventTask.configureWith(params).executeBy(taskExecutor) } + private fun postSnapshot() { + val snapshot = snapshot() + mainHandler.post { listener?.onUpdated(snapshot) } + } + // Extension methods *************************************************************************** private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {