Loading events in a loop

This commit is contained in:
Maxime NATUREL 2023-01-18 14:25:40 +01:00
parent 5473789577
commit 3e118f24ad
18 changed files with 169 additions and 168 deletions

View file

@ -17,9 +17,9 @@
package org.matrix.android.sdk.api.session.room.poll
/**
* Status to indicate loading of polls for a room.
* Represent the status of the loaded polls for a room.
*/
data class LoadedPollsStatus(
val canLoadMore: Boolean,
val nbLoadedDays: Int,
val nbSyncedDays: Int,
)

View file

@ -32,12 +32,6 @@ interface PollHistoryService {
*/
suspend fun loadMore(): LoadedPollsStatus
/**
* Indicate whether loading more polls is possible. If not possible,
* it indicates the end of the room has been reached in the past.
*/
fun canLoadMore(): Boolean
/**
* Get the current status of the loaded polls.
*/

View file

@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.database.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import org.matrix.android.sdk.internal.session.room.poll.PollConstants
/**
* Keeps track of the loading process of the poll history.
@ -35,9 +36,9 @@ internal open class PollHistoryStatusEntity(
var currentTimestampTargetBackwardMs: Long? = null,
/**
* Timestamp of the last completed poll sync target in backward direction in milliseconds.
* Timestamp of the oldest event synced in milliseconds.
*/
var lastTimestampTargetBackwardMs: Long? = null,
var oldestTimestampReachedMs: Long? = null,
/**
* Indicate whether all polls in a room have been synced in backward direction.
@ -57,11 +58,25 @@ internal open class PollHistoryStatusEntity(
companion object
/**
* Create a new instance of the entity with the same content.
*/
fun copy(): PollHistoryStatusEntity {
return PollHistoryStatusEntity(
roomId = roomId,
currentTimestampTargetBackwardMs = currentTimestampTargetBackwardMs,
oldestTimestampReachedMs = oldestTimestampReachedMs,
isEndOfPollsBackward = isEndOfPollsBackward,
tokenEndBackward = tokenEndBackward,
tokenStartForward = tokenStartForward,
)
}
/**
* Indicate whether at least one poll sync has been fully completed backward for the given room.
*/
val hasCompletedASyncBackward: Boolean
get() = lastTimestampTargetBackwardMs != null
get() = oldestTimestampReachedMs != null
/**
* Indicate whether all polls in a room have been synced for the current timestamp target in backward direction.
@ -71,8 +86,20 @@ internal open class PollHistoryStatusEntity(
private fun checkIfCurrentTimestampTargetBackwardIsReached(): Boolean {
val currentTarget = currentTimestampTargetBackwardMs
val lastTarget = lastTimestampTargetBackwardMs
val lastTarget = oldestTimestampReachedMs
// last timestamp target should be older or equal to the current target
return currentTarget != null && lastTarget != null && lastTarget <= currentTarget
}
/**
* Compute the number of days of history currently synced.
*/
fun getNbSyncedDays(currentMs: Long): Int {
val oldestTimestamp = oldestTimestampReachedMs
return if (oldestTimestamp == null) {
0
} else {
((currentMs - oldestTimestamp).coerceAtLeast(0) / PollConstants.MILLISECONDS_PER_DAY).toInt()
}
}
}

View file

@ -89,7 +89,7 @@ internal interface RoomAPI {
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/messages")
suspend fun getRoomMessagesFrom(
@Path("roomId") roomId: String,
@Query("from") from: String,
@Query("from") from: String?,
@Query("dir") dir: String,
@Query("limit") limit: Int?,
@Query("filter") filter: String?,

View file

@ -49,7 +49,6 @@ internal class DefaultPollHistoryService @AssistedInject constructor(
get() = LOADING_PERIOD_IN_DAYS
override suspend fun loadMore(): LoadedPollsStatus {
// TODO when to set currentTimestampMs and who is responsible for it?
val params = LoadMorePollsTask.Params(
roomId = roomId,
currentTimestampMs = clock.epochMillis(),
@ -59,10 +58,6 @@ internal class DefaultPollHistoryService @AssistedInject constructor(
return loadMorePollsTask.execute(params)
}
override fun canLoadMore(): Boolean {
TODO("Not yet implemented")
}
override fun getLoadedPollsStatus(): LoadedPollsStatus {
TODO("Not yet implemented")
}

View file

@ -21,6 +21,12 @@ import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus
import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntity
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.poll.PollConstants.MILLISECONDS_PER_DAY
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import javax.inject.Inject
@ -34,30 +40,38 @@ internal interface LoadMorePollsTask : Task<LoadMorePollsTask.Params, LoadedPoll
)
}
private const val MILLISECONDS_PER_DAY = 24 * 60 * 60_000
internal class DefaultLoadMorePollsTask @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
private val roomAPI: RoomAPI,
private val globalErrorReceiver: GlobalErrorReceiver,
) : LoadMorePollsTask {
override suspend fun execute(params: LoadMorePollsTask.Params): LoadedPollsStatus {
updatePollHistoryStatus(params)
var currentPollHistoryStatus = updatePollHistoryStatus(params)
// TODO fetch events in a loop using current poll history status
// decrypt events and filter in only polls to store them in local
// parse the response to update poll history status
while (shouldFetchMoreEventsBackward(currentPollHistoryStatus)) {
currentPollHistoryStatus = fetchMorePollEventsBackward(params, currentPollHistoryStatus)
}
// TODO
// unmock and check how it behaves when cancelling the process: it should resume where it was stopped
// check the network calls done using Flipper
// check forward of error in case of call api failure
return LoadedPollsStatus(
canLoadMore = true,
nbLoadedDays = 10,
canLoadMore = currentPollHistoryStatus.isEndOfPollsBackward.not(),
nbSyncedDays = currentPollHistoryStatus.getNbSyncedDays(params.currentTimestampMs),
)
}
private suspend fun updatePollHistoryStatus(params: LoadMorePollsTask.Params) {
monarchy.awaitTransaction { realm ->
private fun shouldFetchMoreEventsBackward(status: PollHistoryStatusEntity): Boolean {
return status.currentTimestampTargetBackwardReached.not() && status.isEndOfPollsBackward.not()
}
private suspend fun updatePollHistoryStatus(params: LoadMorePollsTask.Params): PollHistoryStatusEntity {
return monarchy.awaitTransaction { realm ->
val status = PollHistoryStatusEntity.getOrCreate(realm, params.roomId)
val currentTargetTimestampMs = status.currentTimestampTargetBackwardMs
val lastTargetTimestampMs = status.lastTimestampTargetBackwardMs
val lastTargetTimestampMs = status.oldestTimestampReachedMs
val loadingPeriodMs: Long = MILLISECONDS_PER_DAY * params.loadingPeriodInDays.toLong()
if (currentTargetTimestampMs == null) {
// first load, compute the target timestamp
@ -66,6 +80,60 @@ internal class DefaultLoadMorePollsTask @Inject constructor(
// previous load has finished, update the target timestamp
status.currentTimestampTargetBackwardMs = lastTargetTimestampMs - loadingPeriodMs
}
// return a copy of the Realm object
status.copy()
}
}
private suspend fun fetchMorePollEventsBackward(
params: LoadMorePollsTask.Params,
status: PollHistoryStatusEntity
): PollHistoryStatusEntity {
val chunk = executeRequest(globalErrorReceiver) {
roomAPI.getRoomMessagesFrom(
roomId = params.roomId,
from = status.tokenEndBackward,
dir = PaginationDirection.BACKWARDS.value,
limit = params.eventsPageSize,
filter = null
)
}
// TODO decrypt events and filter in only polls to store them in local: see to mutualize with FetchPollResponseEventsTask
return updatePollHistoryStatus(roomId = params.roomId, paginationResponse = chunk)
}
private suspend fun updatePollHistoryStatus(roomId: String, paginationResponse: PaginationResponse): PollHistoryStatusEntity {
return monarchy.awaitTransaction { realm ->
val status = PollHistoryStatusEntity.getOrCreate(realm, roomId)
val tokenStartForward = status.tokenStartForward
if (tokenStartForward == null) {
// save the start token for next forward call
status.tokenEndBackward = paginationResponse.start
}
val oldestEventTimestamp = paginationResponse.events
.minByOrNull { it.originServerTs ?: Long.MAX_VALUE }
?.originServerTs
val currentTargetTimestamp = status.currentTimestampTargetBackwardMs
if (paginationResponse.end == null) {
// start of the timeline is reached, there are no more events
status.isEndOfPollsBackward = true
status.oldestTimestampReachedMs = oldestEventTimestamp
} else if(oldestEventTimestamp != null && currentTargetTimestamp != null && oldestEventTimestamp <= currentTargetTimestamp) {
// target has been reached
status.oldestTimestampReachedMs = oldestEventTimestamp
status.tokenEndBackward = paginationResponse.end
} else {
status.tokenEndBackward = paginationResponse.end
}
// return a copy of the Realm object
status.copy()
}
}
}

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2023 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.session.room.poll
object PollConstants {
const val MILLISECONDS_PER_DAY = 24 * 60 * 60_000
}

View file

@ -58,7 +58,7 @@ class RoomPollsViewModel @AssistedInject constructor(
setState {
copy(
canLoadMore = loadedPollsStatus.canLoadMore,
nbLoadedDays = loadedPollsStatus.nbLoadedDays
nbSyncedDays = loadedPollsStatus.nbSyncedDays,
)
}
}
@ -96,7 +96,7 @@ class RoomPollsViewModel @AssistedInject constructor(
setState {
copy(
canLoadMore = status.canLoadMore,
nbLoadedDays = status.nbLoadedDays,
nbSyncedDays = status.nbSyncedDays,
)
}
}

View file

@ -25,7 +25,7 @@ data class RoomPollsViewState(
val polls: List<PollSummary> = emptyList(),
val isLoadingMore: Boolean = false,
val canLoadMore: Boolean = true,
val nbLoadedDays: Int = 0,
val nbSyncedDays: Int = 0,
val isSyncing: Boolean = false,
) : MavericksState {

View file

@ -16,7 +16,6 @@
package im.vector.app.features.roomprofile.polls.list.data
data class LoadedPollsStatus(
val canLoadMore: Boolean,
val nbLoadedDays: Int,
)
sealed class PollHistoryError : Exception() {
object LoadingError : PollHistoryError()
}

View file

@ -17,25 +17,23 @@
package im.vector.app.features.roomprofile.polls.list.data
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus
import org.matrix.android.sdk.api.session.room.poll.PollHistoryService
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
// TODO add unit tests
class RoomPollDataSource @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder,
) {
private val pollsFlow = MutableSharedFlow<List<PollSummary>>(replay = 1)
private val polls = mutableListOf<PollSummary>()
private var fakeLoadCounter = 0
private fun getPollHistoryService(roomId: String): PollHistoryService? {
@ -46,7 +44,7 @@ class RoomPollDataSource @Inject constructor(
}
// TODO
// unmock using SDK service + add unit tests
// unmock using SDK service
// after unmock, expose domain layer model (entity) and do the mapping to PollSummary in the UI layer
fun getPolls(roomId: String): Flow<List<PollSummary>> {
Timber.d("roomId=$roomId")
@ -55,9 +53,10 @@ class RoomPollDataSource @Inject constructor(
fun getLoadedPollsStatus(roomId: String): LoadedPollsStatus {
Timber.d("roomId=$roomId")
// TODO unmock using SDK
return LoadedPollsStatus(
canLoadMore = canLoadMore(),
nbLoadedDays = fakeLoadCounter * 30,
nbSyncedDays = fakeLoadCounter * 30,
)
}
@ -66,123 +65,13 @@ class RoomPollDataSource @Inject constructor(
}
suspend fun loadMorePolls(roomId: String): LoadedPollsStatus {
getPollHistoryService(roomId)?.loadMore()
// TODO
// remove mocked data + add unit tests
delay(3000)
fakeLoadCounter++
when (fakeLoadCounter) {
1 -> polls.addAll(getActivePollsPart1() + getEndedPollsPart1())
2 -> polls.addAll(getActivePollsPart2() + getEndedPollsPart2())
else -> Unit
}
pollsFlow.emit(polls)
return getLoadedPollsStatus(roomId)
}
private fun getActivePollsPart1(): List<PollSummary.ActivePoll> {
return listOf(
PollSummary.ActivePoll(
id = "id1",
// 2022/06/28 UTC+1
creationTimestamp = 1656367200000,
title = "Which charity would you like to support?"
),
PollSummary.ActivePoll(
id = "id2",
// 2022/06/26 UTC+1
creationTimestamp = 1656194400000,
title = "Which sport should the pupils do this year?"
),
)
}
private fun getActivePollsPart2(): List<PollSummary.ActivePoll> {
return listOf(
PollSummary.ActivePoll(
id = "id3",
// 2022/06/24 UTC+1
creationTimestamp = 1656021600000,
title = "What type of food should we have at the party?"
),
PollSummary.ActivePoll(
id = "id4",
// 2022/06/22 UTC+1
creationTimestamp = 1655848800000,
title = "What film should we show at the end of the year party?"
),
)
}
private fun getEndedPollsPart1(): List<PollSummary.EndedPoll> {
return listOf(
PollSummary.EndedPoll(
id = "id1-ended",
// 2022/06/28 UTC+1
creationTimestamp = 1656367200000,
title = "Which charity would you like to support?",
totalVotes = 22,
winnerOptions = listOf(
PollOptionViewState.PollEnded(
optionId = "id1",
optionAnswer = "Cancer research",
voteCount = 13,
votePercentage = 13 / 22.0,
isWinner = true,
)
),
),
)
}
private fun getEndedPollsPart2(): List<PollSummary.EndedPoll> {
return listOf(
PollSummary.EndedPoll(
id = "id2-ended",
// 2022/06/26 UTC+1
creationTimestamp = 1656194400000,
title = "Where should we do the offsite?",
totalVotes = 92,
winnerOptions = listOf(
PollOptionViewState.PollEnded(
optionId = "id1",
optionAnswer = "Hawaii",
voteCount = 43,
votePercentage = 43 / 92.0,
isWinner = true,
)
),
),
PollSummary.EndedPoll(
id = "id3-ended",
// 2022/06/24 UTC+1
creationTimestamp = 1656021600000,
title = "What type of food should we have at the party?",
totalVotes = 22,
winnerOptions = listOf(
PollOptionViewState.PollEnded(
optionId = "id1",
optionAnswer = "Brazilian",
voteCount = 13,
votePercentage = 13 / 22.0,
isWinner = true,
)
),
),
)
return getPollHistoryService(roomId)?.loadMore() ?: throw PollHistoryError.LoadingError
}
suspend fun syncPolls(roomId: String) {
Timber.d("roomId=$roomId")
// TODO
// unmock using SDK service + add unit tests
if (fakeLoadCounter == 0) {
// fake first load
loadMorePolls(roomId)
} else {
// fake sync
delay(3000)
}
// TODO unmock using SDK service
// fake sync
delay(1000)
}
}

View file

@ -18,6 +18,7 @@ package im.vector.app.features.roomprofile.polls.list.data
import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
import kotlinx.coroutines.flow.Flow
import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus
import javax.inject.Inject
class RoomPollRepository @Inject constructor(

View file

@ -16,8 +16,8 @@
package im.vector.app.features.roomprofile.polls.list.domain
import im.vector.app.features.roomprofile.polls.list.data.LoadedPollsStatus
import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository
import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus
import javax.inject.Inject
class GetLoadedPollsStatusUseCase @Inject constructor(

View file

@ -16,8 +16,8 @@
package im.vector.app.features.roomprofile.polls.list.domain
import im.vector.app.features.roomprofile.polls.list.data.LoadedPollsStatus
import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository
import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus
import javax.inject.Inject
class LoadMorePollsUseCase @Inject constructor(

View file

@ -78,7 +78,7 @@ abstract class RoomPollsListFragment :
views.roomPollsList.configureWith(roomPollsController)
views.roomPollsEmptyTitle.text = getEmptyListTitle(
canLoadMore = viewState.canLoadMore,
nbLoadedDays = viewState.nbLoadedDays,
nbLoadedDays = viewState.nbSyncedDays,
)
}
@ -117,7 +117,7 @@ abstract class RoomPollsListFragment :
roomPollsController.setData(viewState)
views.roomPollsEmptyTitle.text = getEmptyListTitle(
canLoadMore = viewState.canLoadMore,
nbLoadedDays = viewState.nbLoadedDays,
nbLoadedDays = viewState.nbSyncedDays,
)
views.roomPollsEmptyTitle.isVisible = !viewState.isSyncing && viewState.hasNoPolls()
views.roomPollsLoadMoreWhenEmpty.isVisible = viewState.hasNoPollsAndCanLoadMore()

View file

@ -68,7 +68,7 @@ class RoomPollsViewModelTest {
val expectedViewState = initialState.copy(
polls = polls,
canLoadMore = loadedPollsStatus.canLoadMore,
nbLoadedDays = loadedPollsStatus.nbLoadedDays,
nbSyncedDays = loadedPollsStatus.nbLoadedDays,
)
// When
@ -116,7 +116,7 @@ class RoomPollsViewModelTest {
val stateAfterInit = initialState.copy(
polls = polls,
canLoadMore = loadedPollsStatus.canLoadMore,
nbLoadedDays = loadedPollsStatus.nbLoadedDays,
nbSyncedDays = loadedPollsStatus.nbLoadedDays,
)
// When
@ -128,7 +128,7 @@ class RoomPollsViewModelTest {
.assertStatesChanges(
stateAfterInit,
{ copy(isLoadingMore = true) },
{ copy(canLoadMore = newLoadedPollsStatus.canLoadMore, nbLoadedDays = newLoadedPollsStatus.nbLoadedDays) },
{ copy(canLoadMore = newLoadedPollsStatus.canLoadMore, nbSyncedDays = newLoadedPollsStatus.nbLoadedDays) },
{ copy(isLoadingMore = false) },
)
.finish()

View file

@ -16,13 +16,13 @@
package im.vector.app.features.roomprofile.polls.list.domain
import im.vector.app.features.roomprofile.polls.list.data.LoadedPollsStatus
import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus
class GetLoadedPollsStatusUseCaseTest {
@ -38,7 +38,7 @@ class GetLoadedPollsStatusUseCaseTest {
val aRoomId = "roomId"
val expectedStatus = LoadedPollsStatus(
canLoadMore = true,
nbLoadedDays = 10,
nbSyncedDays = 10,
)
every { fakeRoomPollRepository.getLoadedPollsStatus(aRoomId) } returns expectedStatus

View file

@ -17,11 +17,13 @@
package im.vector.app.features.roomprofile.polls.list.domain
import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository
import io.mockk.coJustRun
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus
class LoadMorePollsUseCaseTest {
@ -35,12 +37,17 @@ class LoadMorePollsUseCaseTest {
fun `given repo when execute then correct method of repo is called`() = runTest {
// Given
val aRoomId = "roomId"
coJustRun { fakeRoomPollRepository.loadMorePolls(aRoomId) }
val loadedPollsStatus = LoadedPollsStatus(
canLoadMore = true,
nbSyncedDays = 10,
)
coEvery { fakeRoomPollRepository.loadMorePolls(aRoomId) } returns loadedPollsStatus
// When
loadMorePollsUseCase.execute(aRoomId)
val result = loadMorePollsUseCase.execute(aRoomId)
// Then
result shouldBeEqualTo loadedPollsStatus
coVerify { fakeRoomPollRepository.loadMorePolls(aRoomId) }
}
}