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
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.
@ -43,7 +43,7 @@ interface PollHistoryService {
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
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.realm.kotlin.where
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.poll.LoadedPollsStatus
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 timber.log.Timber
private const val LOADING_PERIOD_IN_DAYS = 30
private const val EVENTS_PAGE_SIZE = 250
@ -32,9 +46,11 @@ private const val EVENTS_PAGE_SIZE = 250
// TODO add unit tests
internal class DefaultPollHistoryService @AssistedInject constructor(
@Assisted private val roomId: String,
@SessionDatabase private val monarchy: Monarchy,
private val clock: Clock,
private val loadMorePollsTask: LoadMorePollsTask,
private val getLoadedPollsStatusTask: GetLoadedPollsStatusTask,
private val timelineEventMapper: TimelineEventMapper,
) : PollHistoryService {
@AssistedFactory
@ -68,7 +84,38 @@ internal class DefaultPollHistoryService @AssistedInject constructor(
delay(1000)
}
override fun getPolls(): LiveData<List<PollResponseAggregatedSummary>> {
TODO("listen database and update query depending on latest PollHistoryStatusEntity.oldestTimestampReachedMs")
override fun getPollEvents(): LiveData<List<TimelineEvent>> {
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) {
// start of the timeline is reached, there are no more events
status.isEndOfPollsBackward = true
status.oldestTimestampReachedMs = oldestEventTimestamp
if(oldestEventTimestamp != null && oldestEventTimestamp > 0) {
status.oldestTimestampReachedMs = oldestEventTimestamp
}
} else if (oldestEventTimestamp != null && currentTargetTimestamp != null && oldestEventTimestamp <= currentTargetTimestamp) {
// target has been reached
status.oldestTimestampReachedMs = oldestEventTimestamp

View file

@ -92,6 +92,7 @@ class PollItemViewStateFactory @Inject constructor(
question = question,
votesStatus = totalVotesText,
canVote = false,
// TODO extract into helper method or mapper
optionViewStates = pollCreationInfo?.answers?.map { answer ->
val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "")
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.hiltMavericksViewModelFactory
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.LoadMorePollsUseCase
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.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class RoomPollsViewModel @AssistedInject constructor(
@Assisted initialState: RoomPollsViewState,
private val getPollsUseCase: GetPollsUseCase,
private val getLoadedPollsStatusUseCase: GetLoadedPollsStatusUseCase,
private val loadMorePollsUseCase: LoadMorePollsUseCase,
private val syncPollsUseCase: SyncPollsUseCase,
private val pollSummaryMapper: PollSummaryMapper,
) : VectorViewModel<RoomPollsViewState, RoomPollsAction, RoomPollsViewEvent>(initialState) {
@AssistedFactory
@ -73,6 +74,7 @@ class RoomPollsViewModel @AssistedInject constructor(
private fun observePolls(roomId: String) {
getPollsUseCase.execute(roomId)
.map { it.map { event -> pollSummaryMapper.map(event) } }
.onEach { setState { copy(polls = it) } }
.launchIn(viewModelScope)
}

View file

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

View file

@ -16,17 +16,16 @@
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 org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
class RoomPollRepository @Inject constructor(
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<PollSummary>> {
fun getPolls(roomId: String): Flow<List<TimelineEvent>> {
return roomPollDataSource.getPolls(roomId)
}

View file

@ -17,17 +17,17 @@
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.ui.PollSummary
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
class GetPollsUseCase @Inject constructor(
private val roomPollRepository: RoomPollRepository,
) {
fun execute(roomId: String): Flow<List<PollSummary>> {
fun execute(roomId: String): Flow<List<TimelineEvent>> {
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,
)
}
}
}