Observation of the local events to render UI

This commit is contained in:
Maxime NATUREL 2023-01-19 16:10:33 +01:00
parent 7ca532a5f6
commit 96252ec2af
9 changed files with 153 additions and 26 deletions

View file

@ -17,7 +17,7 @@
package org.matrix.android.sdk.api.session.room.poll package org.matrix.android.sdk.api.session.room.poll
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.room.model.PollResponseAggregatedSummary import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
/** /**
* Expose methods to get history of polls in rooms. * Expose methods to get history of polls in rooms.
@ -43,7 +43,7 @@ interface PollHistoryService {
suspend fun syncPolls() suspend fun syncPolls()
/** /**
* Get currently loaded list of polls. See [loadMore]. * Get currently loaded list of poll events. See [loadMore].
*/ */
fun getPolls(): LiveData<List<PollResponseAggregatedSummary>> fun getPollEvents(): LiveData<List<TimelineEvent>>
} }

View file

@ -17,14 +17,28 @@
package org.matrix.android.sdk.internal.session.room.poll package org.matrix.android.sdk.internal.session.room.poll
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.realm.kotlin.where
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.model.PollResponseAggregatedSummary import org.matrix.android.sdk.api.session.room.model.PollResponseAggregatedSummary
import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus
import org.matrix.android.sdk.api.session.room.poll.PollHistoryService import org.matrix.android.sdk.api.session.room.poll.PollHistoryService
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.database.mapper.PollResponseAggregatedSummaryEntityMapper
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntity
import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntityFields
import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
private const val LOADING_PERIOD_IN_DAYS = 30 private const val LOADING_PERIOD_IN_DAYS = 30
private const val EVENTS_PAGE_SIZE = 250 private const val EVENTS_PAGE_SIZE = 250
@ -32,9 +46,11 @@ private const val EVENTS_PAGE_SIZE = 250
// TODO add unit tests // TODO add unit tests
internal class DefaultPollHistoryService @AssistedInject constructor( internal class DefaultPollHistoryService @AssistedInject constructor(
@Assisted private val roomId: String, @Assisted private val roomId: String,
@SessionDatabase private val monarchy: Monarchy,
private val clock: Clock, private val clock: Clock,
private val loadMorePollsTask: LoadMorePollsTask, private val loadMorePollsTask: LoadMorePollsTask,
private val getLoadedPollsStatusTask: GetLoadedPollsStatusTask, private val getLoadedPollsStatusTask: GetLoadedPollsStatusTask,
private val timelineEventMapper: TimelineEventMapper,
) : PollHistoryService { ) : PollHistoryService {
@AssistedFactory @AssistedFactory
@ -68,7 +84,38 @@ internal class DefaultPollHistoryService @AssistedInject constructor(
delay(1000) delay(1000)
} }
override fun getPolls(): LiveData<List<PollResponseAggregatedSummary>> { override fun getPollEvents(): LiveData<List<TimelineEvent>> {
TODO("listen database and update query depending on latest PollHistoryStatusEntity.oldestTimestampReachedMs") val pollHistoryStatusLiveData = getPollHistoryStatus()
return Transformations.switchMap(pollHistoryStatusLiveData) { results ->
val oldestTimestamp = results.firstOrNull()?.oldestTimestampReachedMs ?: clock.epochMillis()
Timber.d("oldestTimestamp=$oldestTimestamp")
monarchy.findAllMappedWithChanges(
{ realm ->
val pollTypes = EventType.POLL_START.values.toTypedArray()
realm.where<TimelineEventEntity>()
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
.`in`(TimelineEventEntityFields.ROOT.TYPE, pollTypes)
.greaterThan(TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS, oldestTimestamp)
},
{ result ->
timelineEventMapper.map(result, buildReadReceipts = false)
}
)
}
}
private fun getPollHistoryStatus(): LiveData<List<PollHistoryStatusEntity>> {
return monarchy.findAllMappedWithChanges(
{ realm ->
realm.where<PollHistoryStatusEntity>()
.equalTo(PollHistoryStatusEntityFields.ROOM_ID, roomId)
},
{ result ->
// make a copy of the Realm object since it will be used in another transformations
result.copy()
}
)
} }
} }

View file

@ -137,7 +137,9 @@ internal class DefaultLoadMorePollsTask @Inject constructor(
if (paginationResponse.end == null) { if (paginationResponse.end == null) {
// start of the timeline is reached, there are no more events // start of the timeline is reached, there are no more events
status.isEndOfPollsBackward = true status.isEndOfPollsBackward = true
status.oldestTimestampReachedMs = oldestEventTimestamp if(oldestEventTimestamp != null && oldestEventTimestamp > 0) {
status.oldestTimestampReachedMs = oldestEventTimestamp
}
} else if (oldestEventTimestamp != null && currentTargetTimestamp != null && oldestEventTimestamp <= currentTargetTimestamp) { } else if (oldestEventTimestamp != null && currentTargetTimestamp != null && oldestEventTimestamp <= currentTargetTimestamp) {
// target has been reached // target has been reached
status.oldestTimestampReachedMs = oldestEventTimestamp status.oldestTimestampReachedMs = oldestEventTimestamp

View file

@ -92,6 +92,7 @@ class PollItemViewStateFactory @Inject constructor(
question = question, question = question,
votesStatus = totalVotesText, votesStatus = totalVotesText,
canVote = false, canVote = false,
// TODO extract into helper method or mapper
optionViewStates = pollCreationInfo?.answers?.map { answer -> optionViewStates = pollCreationInfo?.answers?.map { answer ->
val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "") val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "")
PollOptionViewState.PollEnded( PollOptionViewState.PollEnded(

View file

@ -23,20 +23,21 @@ import dagger.assisted.AssistedInject
import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.roomprofile.polls.list.domain.GetLoadedPollsStatusUseCase
import im.vector.app.features.roomprofile.polls.list.domain.GetPollsUseCase import im.vector.app.features.roomprofile.polls.list.domain.GetPollsUseCase
import im.vector.app.features.roomprofile.polls.list.domain.LoadMorePollsUseCase import im.vector.app.features.roomprofile.polls.list.domain.LoadMorePollsUseCase
import im.vector.app.features.roomprofile.polls.list.domain.SyncPollsUseCase import im.vector.app.features.roomprofile.polls.list.domain.SyncPollsUseCase
import im.vector.app.features.roomprofile.polls.list.ui.PollSummaryMapper
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class RoomPollsViewModel @AssistedInject constructor( class RoomPollsViewModel @AssistedInject constructor(
@Assisted initialState: RoomPollsViewState, @Assisted initialState: RoomPollsViewState,
private val getPollsUseCase: GetPollsUseCase, private val getPollsUseCase: GetPollsUseCase,
private val getLoadedPollsStatusUseCase: GetLoadedPollsStatusUseCase,
private val loadMorePollsUseCase: LoadMorePollsUseCase, private val loadMorePollsUseCase: LoadMorePollsUseCase,
private val syncPollsUseCase: SyncPollsUseCase, private val syncPollsUseCase: SyncPollsUseCase,
private val pollSummaryMapper: PollSummaryMapper,
) : VectorViewModel<RoomPollsViewState, RoomPollsAction, RoomPollsViewEvent>(initialState) { ) : VectorViewModel<RoomPollsViewState, RoomPollsAction, RoomPollsViewEvent>(initialState) {
@AssistedFactory @AssistedFactory
@ -73,6 +74,7 @@ class RoomPollsViewModel @AssistedInject constructor(
private fun observePolls(roomId: String) { private fun observePolls(roomId: String) {
getPollsUseCase.execute(roomId) getPollsUseCase.execute(roomId)
.map { it.map { event -> pollSummaryMapper.map(event) } }
.onEach { setState { copy(polls = it) } } .onEach { setState { copy(polls = it) } }
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }

View file

@ -16,15 +16,13 @@
package im.vector.app.features.roomprofile.polls.list.data package im.vector.app.features.roomprofile.polls.list.data
import androidx.lifecycle.asFlow
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
import kotlinx.coroutines.flow.Flow 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.getRoom
import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus
import org.matrix.android.sdk.api.session.room.poll.PollHistoryService import org.matrix.android.sdk.api.session.room.poll.PollHistoryService
import timber.log.Timber import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject import javax.inject.Inject
// TODO add unit tests // TODO add unit tests
@ -32,8 +30,6 @@ class RoomPollDataSource @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
) { ) {
private val pollsFlow = MutableSharedFlow<List<PollSummary>>(replay = 1)
private fun getPollHistoryService(roomId: String): PollHistoryService { private fun getPollHistoryService(roomId: String): PollHistoryService {
return activeSessionHolder return activeSessionHolder
.getSafeActiveSession() .getSafeActiveSession()
@ -42,12 +38,8 @@ class RoomPollDataSource @Inject constructor(
?: throw PollHistoryError.UnknownRoomError ?: throw PollHistoryError.UnknownRoomError
} }
// TODO fun getPolls(roomId: String): Flow<List<TimelineEvent>> {
// unmock using SDK service return getPollHistoryService(roomId).getPollEvents().asFlow()
// 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")
return pollsFlow.asSharedFlow()
} }
suspend fun getLoadedPollsStatus(roomId: String): LoadedPollsStatus { suspend fun getLoadedPollsStatus(roomId: String): LoadedPollsStatus {

View file

@ -16,17 +16,16 @@
package im.vector.app.features.roomprofile.polls.list.data 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 kotlinx.coroutines.flow.Flow
import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject import javax.inject.Inject
class RoomPollRepository @Inject constructor( class RoomPollRepository @Inject constructor(
private val roomPollDataSource: RoomPollDataSource, private val roomPollDataSource: RoomPollDataSource,
) { ) {
// TODO after unmock, expose domain layer model (entity) and do the mapping to PollSummary in the UI layer fun getPolls(roomId: String): Flow<List<TimelineEvent>> {
fun getPolls(roomId: String): Flow<List<PollSummary>> {
return roomPollDataSource.getPolls(roomId) return roomPollDataSource.getPolls(roomId)
} }

View file

@ -17,17 +17,17 @@
package im.vector.app.features.roomprofile.polls.list.domain package im.vector.app.features.roomprofile.polls.list.domain
import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository
import im.vector.app.features.roomprofile.polls.list.ui.PollSummary
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject import javax.inject.Inject
class GetPollsUseCase @Inject constructor( class GetPollsUseCase @Inject constructor(
private val roomPollRepository: RoomPollRepository, private val roomPollRepository: RoomPollRepository,
) { ) {
fun execute(roomId: String): Flow<List<PollSummary>> { fun execute(roomId: String): Flow<List<TimelineEvent>> {
return roomPollRepository.getPolls(roomId) return roomPollRepository.getPolls(roomId)
.map { it.sortedByDescending { poll -> poll.creationTimestamp } } .map { it.sortedByDescending { event -> event.root.originServerTs } }
} }
} }

View file

@ -0,0 +1,84 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.roomprofile.polls.list.ui
import im.vector.app.core.extensions.getVectorLastMessageContent
import im.vector.app.features.home.room.detail.timeline.helper.PollResponseDataFactory
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
// TODO add unit tests
class PollSummaryMapper @Inject constructor(
private val pollResponseDataFactory: PollResponseDataFactory,
) {
fun map(timelineEvent: TimelineEvent): PollSummary {
val content = timelineEvent.getVectorLastMessageContent()
val pollResponseData = pollResponseDataFactory.create(timelineEvent)
val eventId = timelineEvent.root.eventId.orEmpty()
val creationTimestamp = timelineEvent.root.originServerTs ?: 0
if (eventId.isNotEmpty() && creationTimestamp > 0 && content is MessagePollContent && pollResponseData != null) {
return convertToPollSummary(
eventId = eventId,
creationTimestamp = creationTimestamp,
messagePollContent = content,
pollResponseData = pollResponseData
)
} else {
throw IllegalStateException("expected MessagePollContent")
}
}
private fun convertToPollSummary(
eventId: String,
creationTimestamp: Long,
messagePollContent: MessagePollContent,
pollResponseData: PollResponseData
): PollSummary {
val pollCreationInfo = messagePollContent.getBestPollCreationInfo()
val pollTitle = pollCreationInfo?.question?.getBestQuestion().orEmpty()
return if (pollResponseData.isClosed) {
val winnerVoteCount = pollResponseData.winnerVoteCount
PollSummary.EndedPoll(
id = eventId,
creationTimestamp = creationTimestamp,
title = pollTitle,
totalVotes = pollResponseData.totalVotes,
// TODO mutualise this with PollItemViewStateFactory
winnerOptions = pollCreationInfo?.answers?.map { answer ->
val voteSummary = pollResponseData.getVoteSummaryOfAnOption(answer.id ?: "")
PollOptionViewState.PollEnded(
optionId = answer.id ?: "",
optionAnswer = answer.getBestAnswer() ?: "",
voteCount = voteSummary?.total ?: 0,
votePercentage = voteSummary?.percentage ?: 0.0,
isWinner = winnerVoteCount != 0 && voteSummary?.total == winnerVoteCount
)
} ?: emptyList()
)
} else {
PollSummary.ActivePoll(
id = eventId,
creationTimestamp = creationTimestamp,
title = pollTitle,
)
}
}
}