Timeline : make tests compile and pass

This commit is contained in:
ganfra 2019-04-01 15:18:52 +02:00
parent 94db36d6c4
commit be6a4efacb
8 changed files with 150 additions and 69 deletions

View File

@ -28,6 +28,7 @@ android {
targetSdkVersion 28 targetSdkVersion 28
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"


} }
@ -104,17 +105,17 @@ dependencies {
testImplementation 'org.robolectric:shadows-support-v4:3.0' testImplementation 'org.robolectric:shadows-support-v4:3.0'
testImplementation "io.mockk:mockk:1.8.13.kotlin13" testImplementation "io.mockk:mockk:1.8.13.kotlin13"
testImplementation 'org.amshove.kluent:kluent-android:1.44' 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" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"


androidTestImplementation "org.koin:koin-test:$koin_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:runner:1.1.1'
androidTestImplementation 'androidx.test:rules: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 'androidx.test.espresso:espresso-core:3.1.1'
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44' androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
androidTestImplementation "io.mockk:mockk-android:1.8.13.kotlin13" androidTestImplementation "io.mockk:mockk-android:1.8.13.kotlin13"
androidTestImplementation "androidx.arch.core:core-testing:$lifecycle_version" androidTestImplementation "androidx.arch.core:core-testing:$lifecycle_version"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"



} }

View File

@ -16,10 +16,9 @@


package im.vector.matrix.android.session.room.timeline package im.vector.matrix.android.session.room.timeline


import androidx.test.ext.junit.runners.AndroidJUnit4
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest 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.add
import im.vector.matrix.android.internal.database.helper.addAll import im.vector.matrix.android.internal.database.helper.addAll
import im.vector.matrix.android.internal.database.helper.isUnlinked 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.helper.merge
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection 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.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
@ -35,9 +37,10 @@ import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.shouldEqual import org.amshove.kluent.shouldEqual
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import kotlin.random.Random import org.junit.runner.RunWith




@RunWith(AndroidJUnit4::class)
internal class ChunkEntityTest : InstrumentedTest { internal class ChunkEntityTest : InstrumentedTest {


private lateinit var monarchy: Monarchy private lateinit var monarchy: Monarchy
@ -54,7 +57,7 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldAdd_whenNotAlreadyIncluded() { fun add_shouldAdd_whenNotAlreadyIncluded() {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject() 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 chunk.events.size shouldEqual 1
} }
@ -64,7 +67,7 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldNotAdd_whenAlreadyIncluded() { fun add_shouldNotAdd_whenAlreadyIncluded() {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject() 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.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.events.size shouldEqual 1 chunk.events.size shouldEqual 1
@ -75,7 +78,7 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldStateIndexIncremented_whenStateEventIsAddedForward() { fun add_shouldStateIndexIncremented_whenStateEventIsAddedForward() {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject() val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(true) val fakeEvent = createFakeRoomMemberEvent()
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1 chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1
} }
@ -85,7 +88,7 @@ internal class ChunkEntityTest : InstrumentedTest {
fun add_shouldStateIndexNotIncremented_whenNoStateEventIsAdded() { fun add_shouldStateIndexNotIncremented_whenNoStateEventIsAdded() {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject() 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.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0 chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0
} }
@ -196,15 +199,4 @@ internal class ChunkEntityTest : InstrumentedTest {
} }
} }



private fun createFakeListOfEvents(size: Int = 10): List<Event> {
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)
}

} }

View File

@ -17,9 +17,15 @@
package im.vector.matrix.android.session.room.timeline package im.vector.matrix.android.session.room.timeline


import com.zhuinden.monarchy.Monarchy 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.Event
import im.vector.matrix.android.api.session.events.model.EventType 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.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.addAll
import im.vector.matrix.android.internal.database.helper.addOrUpdate import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
@ -30,27 +36,56 @@ import kotlin.random.Random


object RoomDataHelper { 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<Event> { fun createFakeListOfEvents(size: Int = 10): List<Event> {
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 { fun createFakeEvent(type: String,
val eventId = Random.nextLong(System.currentTimeMillis()).toString() content: Content? = null,
val type = if (asStateEvent) EventType.STATE_ROOM_NAME else EventType.MESSAGE prevContent: Content? = null,
return Event(type, eventId) 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) { fun fakeInitialSync(monarchy: Monarchy, roomId: String) {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val roomEntity = realm.createObject<RoomEntity>(roomId) val roomEntity = realm.createObject<RoomEntity>(roomId)
roomEntity.membership = MyMembership.JOINED roomEntity.membership = MyMembership.JOINED
val eventList = createFakeListOfEvents(30) val eventList = createFakeListOfEvents(10)
val chunkEntity = realm.createObject<ChunkEntity>().apply { val chunkEntity = realm.createObject<ChunkEntity>().apply {
nextToken = null nextToken = null
prevToken = Random.nextLong(System.currentTimeMillis()).toString() prevToken = Random.nextLong(System.currentTimeMillis()).toString()
isLastForward = true isLastForward = true
} }
chunkEntity.addAll("roomId", eventList, PaginationDirection.FORWARDS) chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS)
roomEntity.addOrUpdate(chunkEntity) roomEntity.addOrUpdate(chunkEntity)
} }
} }

View File

@ -16,55 +16,75 @@


package im.vector.matrix.android.session.room.timeline 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 com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.LiveDataTestObserver 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.members.RoomMemberExtractor
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService 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.session.room.timeline.TokenChunkEventPersistor
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.testCoroutineDispatchers import im.vector.matrix.android.testCoroutineDispatchers
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import org.amshove.kluent.shouldEqual
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import timber.log.Timber
import java.util.concurrent.CountDownLatch


internal class TimelineHolderTest : InstrumentedTest { internal class TimelineTest : InstrumentedTest {

companion object {
private const val ROOM_ID = "roomId"
}


@get:Rule val testRule = InstantTaskExecutorRule()
private lateinit var monarchy: Monarchy private lateinit var monarchy: Monarchy


@Before @Before
fun setup() { fun setup() {
Timber.plant(Timber.DebugTree())
Realm.init(context()) Realm.init(context())
val testConfiguration = RealmConfiguration.Builder().name("test-realm").build() val testConfiguration = RealmConfiguration.Builder().name("test-realm").build()
Realm.deleteRealm(testConfiguration) Realm.deleteRealm(testConfiguration)
monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build() monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build()
RoomDataHelper.fakeInitialSync(monarchy, ROOM_ID)
} }


@Test private fun createTimeline(initialEventId: String? = null): Timeline {
@UiThreadTest
fun backPaginate_shouldLoadMoreEvents_whenLoadAroundIsCalled() {
val roomId = "roomId"
val taskExecutor = TaskExecutor(testCoroutineDispatchers) val taskExecutor = TaskExecutor(testCoroutineDispatchers)
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy) val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy)
val paginationTask = FakePaginationTask(tokenChunkEventPersistor) val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor) val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
RoomDataHelper.fakeInitialSync(monarchy, roomId) val roomMemberExtractor = RoomMemberExtractor(ROOM_ID)
val timelineHolder = DefaultTimelineService(roomId, monarchy, taskExecutor, getContextOfEventTask, RoomMemberExtractor(roomId)) val timelineEventFactory = TimelineEventFactory(roomMemberExtractor)
val timelineObserver = LiveDataTestObserver.test(timelineHolder.timeline()) return DefaultTimeline(ROOM_ID, initialEventId, monarchy.realmConfiguration, taskExecutor, getContextOfEventTask, timelineEventFactory, paginationTask, null)
timelineObserver.awaitNextValue().assertHasValue() }
var timelineData = timelineObserver.value()
timelineData.events.size shouldEqual 30 @Test
(0 until timelineData.events.size).map { fun backPaginate_shouldLoadMoreEvents_whenPaginateIsCalled() {
timelineData.events.loadAround(it) val timeline = createTimeline()
timeline.start()
val paginationCount = 30
var initialLoad = 0
val latch = CountDownLatch(2)
var timelineEvents: List<TimelineEvent> = emptyList()
timeline.listener = object : Timeline.Listener {
override fun onUpdated(snapshot: List<TimelineEvent>) {
if (snapshot.isNotEmpty()) {
if (initialLoad == 0) {
initialLoad = snapshot.size
}
timelineEvents = snapshot
latch.countDown()
timeline.paginate(Timeline.Direction.BACKWARDS, paginationCount)
}
}
} }
timelineObserver.awaitNextValue().assertHasValue() latch.await()
timelineData = timelineObserver.value() timelineEvents.size shouldEqual initialLoad + paginationCount
timelineData.events.size shouldEqual 60 timeline.dispose()
} }




View File

@ -35,6 +35,18 @@ inline fun <reified T> Content?.toModel(): T? {
} }
} }


/**
* This methods is a facility method to map a model to a json Content
*/
@Suppress("UNCHECKED_CAST")
inline fun <reified T> 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. * 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. * The content and prevContent json fields can easily be mapped to a model with [toModel] method.

View File

@ -54,10 +54,14 @@ internal fun ChunkEntity.merge(roomId: String,
if (direction == PaginationDirection.FORWARDS) { if (direction == PaginationDirection.FORWARDS) {
this.nextToken = chunkToMerge.nextToken this.nextToken = chunkToMerge.nextToken
this.isLastForward = chunkToMerge.isLastForward this.isLastForward = chunkToMerge.isLastForward
this.forwardsStateIndex = chunkToMerge.forwardsStateIndex
this.forwardsDisplayIndex = chunkToMerge.forwardsDisplayIndex
eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
} else { } else {
this.prevToken = chunkToMerge.prevToken this.prevToken = chunkToMerge.prevToken
this.isLastBackward = chunkToMerge.isLastBackward this.isLastBackward = chunkToMerge.isLastBackward
this.backwardsStateIndex = chunkToMerge.backwardsStateIndex
this.backwardsDisplayIndex = chunkToMerge.backwardsDisplayIndex
eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
} }
eventsToMerge.forEach { eventsToMerge.forEach {
@ -111,8 +115,7 @@ internal fun ChunkEntity.add(roomId: String,
this.displayIndex = currentDisplayIndex this.displayIndex = currentDisplayIndex
} }
// We are not using the order of the list, but will be sorting with displayIndex field // 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(eventEntity)
events.add(position, eventEntity)
} }


private fun ChunkEntity.assertIsManaged() { private fun ChunkEntity.assertIsManaged() {

View File

@ -48,7 +48,6 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI,
return if (areAllMembersAlreadyLoaded(params.roomId)) { return if (areAllMembersAlreadyLoaded(params.roomId)) {
Try.just(true) Try.just(true)
} else { } else {
//TODO use this token
val lastToken = syncTokenStore.getLastToken() val lastToken = syncTokenStore.getLastToken()
executeRequest<RoomMembersResponse> { executeRequest<RoomMembersResponse> {
apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value) apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value)
@ -63,7 +62,7 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI,
.tryTransactionSync { realm -> .tryTransactionSync { realm ->
// We ignore all the already known members // We ignore all the already known members
val roomEntity = RoomEntity.where(realm, roomId).findFirst() val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId) ?: realm.createObject(roomId)


val roomMembers = RoomMembers(realm, roomId).getLoaded() val roomMembers = RoomMembers(realm, roomId).getLoaded()
val eventsToInsert = response.roomMemberEvents.filter { !roomMembers.containsKey(it.stateKey) } 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 { private fun areAllMembersAlreadyLoaded(roomId: String): Boolean {
return monarchy return monarchy
.fetchAllCopiedSync { RoomEntity.where(it, roomId) } .fetchAllCopiedSync { RoomEntity.where(it, roomId) }
.firstOrNull() .firstOrNull()
?.areAllMembersLoaded ?: false ?.areAllMembersLoaded ?: false
} }


} }

View File

@ -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.database.query.where
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith 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 timber.log.Timber
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -97,10 +102,14 @@ internal class DefaultTimeline(
val state = getPaginationState(direction) val state = getPaginationState(direction)
if (state.isPaginating) { if (state.isPaginating) {
// We are getting new items from pagination // We are getting new items from pagination
paginateInternal(startDisplayIndex, direction, state.requestedCount) val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedCount)
if (shouldPostSnapshot) {
postSnapshot()
}
} else { } else {
// We are getting new items from sync // We are getting new items from sync
buildTimelineEvents(startDisplayIndex, direction, range.length.toLong()) buildTimelineEvents(startDisplayIndex, direction, range.length.toLong())
postSnapshot()
} }
} }
} }
@ -114,7 +123,10 @@ internal class DefaultTimeline(
} }
Timber.v("Paginate $direction of $count items") Timber.v("Paginate $direction of $count items")
val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex 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 * 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, private fun paginateInternal(startDisplayIndex: Int,
direction: Timeline.Direction, direction: Timeline.Direction,
count: Int) { count: Int): Boolean {
updatePaginationState(direction) { it.copy(requestedCount = count, isPaginating = true) } updatePaginationState(direction) { it.copy(requestedCount = count, isPaginating = true) }
val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong()) val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong())
if (builtCount < count && !hasReachedEnd(direction)) { val shouldFetchMore = builtCount < count && !hasReachedEnd(direction)
if (shouldFetchMore) {
val newRequestedCount = count - builtCount val newRequestedCount = count - builtCount
updatePaginationState(direction) { it.copy(requestedCount = newRequestedCount) } updatePaginationState(direction) { it.copy(requestedCount = newRequestedCount) }
val fetchingCount = Math.max(MIN_FETCHING_COUNT, newRequestedCount) val fetchingCount = Math.max(MIN_FETCHING_COUNT, newRequestedCount)
@ -205,6 +219,7 @@ internal class DefaultTimeline(
} else { } else {
updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) } updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) }
} }
return !shouldFetchMore
} }


private fun snapshot(): List<TimelineEvent> { private fun snapshot(): List<TimelineEvent> {
@ -252,12 +267,13 @@ internal class DefaultTimeline(
} else { } else {
val count = Math.min(INITIAL_LOAD_SIZE, liveEvents.size) val count = Math.min(INITIAL_LOAD_SIZE, liveEvents.size)
if (isLive) { if (isLive) {
paginate(Timeline.Direction.BACKWARDS, count) paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count)
} else { } else {
paginate(Timeline.Direction.FORWARDS, count / 2) paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2)
paginate(Timeline.Direction.BACKWARDS, 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) { private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
val token = getTokenLive(direction) ?: return val token = getTokenLive(direction) ?: return
val params = PaginationTask.Params(roomId = roomId, val params = PaginationTask.Params(roomId = roomId,
from = token, from = token,
direction = direction.toPaginationDirection(), direction = direction.toPaginationDirection(),
limit = limit) limit = limit)


Timber.v("Should fetch $limit items $direction") Timber.v("Should fetch $limit items $direction")
paginationTask.configureWith(params) paginationTask.configureWith(params)
@ -336,8 +352,6 @@ internal class DefaultTimeline(
builtEvents.add(position, timelineEvent) builtEvents.add(position, timelineEvent)
} }
Timber.v("Built ${offsetResults.size} items from db") Timber.v("Built ${offsetResults.size} items from db")
val snapshot = snapshot()
mainHandler.post { listener?.onUpdated(snapshot) }
return offsetResults.size return offsetResults.size
} }


@ -399,6 +413,11 @@ internal class DefaultTimeline(
contextOfEventTask.configureWith(params).executeBy(taskExecutor) contextOfEventTask.configureWith(params).executeBy(taskExecutor)
} }


private fun postSnapshot() {
val snapshot = snapshot()
mainHandler.post { listener?.onUpdated(snapshot) }
}

// Extension methods *************************************************************************** // Extension methods ***************************************************************************


private fun Timeline.Direction.toPaginationDirection(): PaginationDirection { private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {