From 96252ec2af9cd42f27dffce189886a1774a12336 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Thu, 19 Jan 2023 16:10:33 +0100 Subject: [PATCH] Observation of the local events to render UI --- .../session/room/poll/PollHistoryService.kt | 6 +- .../room/poll/DefaultPollHistoryService.kt | 51 ++++++++++- .../session/room/poll/LoadMorePollsTask.kt | 4 +- .../factory/PollItemViewStateFactory.kt | 1 + .../roomprofile/polls/RoomPollsViewModel.kt | 6 +- .../polls/list/data/RoomPollDataSource.kt | 16 +--- .../polls/list/data/RoomPollRepository.kt | 5 +- .../polls/list/domain/GetPollsUseCase.kt | 6 +- .../polls/list/ui/PollSummaryMapper.kt | 84 +++++++++++++++++++ 9 files changed, 153 insertions(+), 26 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummaryMapper.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/PollHistoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/PollHistoryService.kt index 866c3e6b81..bd0d6f4ab5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/PollHistoryService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/PollHistoryService.kt @@ -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> + fun getPollEvents(): LiveData> } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/DefaultPollHistoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/DefaultPollHistoryService.kt index e01d91d1be..add6dd4b90 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/DefaultPollHistoryService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/DefaultPollHistoryService.kt @@ -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> { - TODO("listen database and update query depending on latest PollHistoryStatusEntity.oldestTimestampReachedMs") + override fun getPollEvents(): LiveData> { + 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() + .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> { + return monarchy.findAllMappedWithChanges( + { realm -> + realm.where() + .equalTo(PollHistoryStatusEntityFields.ROOM_ID, roomId) + }, + { result -> + // make a copy of the Realm object since it will be used in another transformations + result.copy() + } + ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/LoadMorePollsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/LoadMorePollsTask.kt index 7870897ace..94b73ff211 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/LoadMorePollsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/LoadMorePollsTask.kt @@ -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 diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt index 7abc51fa51..e4f0fc3ba5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt @@ -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( diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt index fccdef87b8..83b71878cd 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModel.kt @@ -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(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) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSource.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSource.kt index ee3b477685..86f77afc29 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSource.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSource.kt @@ -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>(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> { - Timber.d("roomId=$roomId") - return pollsFlow.asSharedFlow() + fun getPolls(roomId: String): Flow> { + return getPollHistoryService(roomId).getPollEvents().asFlow() } suspend fun getLoadedPollsStatus(roomId: String): LoadedPollsStatus { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepository.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepository.kt index 4679af4434..ff29ffbdc0 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepository.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepository.kt @@ -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> { + fun getPolls(roomId: String): Flow> { return roomPollDataSource.getPolls(roomId) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCase.kt index be2afb226f..0f6316efde 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCase.kt @@ -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> { + fun execute(roomId: String): Flow> { return roomPollRepository.getPolls(roomId) - .map { it.sortedByDescending { poll -> poll.creationTimestamp } } + .map { it.sortedByDescending { event -> event.root.originServerTs } } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummaryMapper.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummaryMapper.kt new file mode 100644 index 0000000000..821620e842 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummaryMapper.kt @@ -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, + ) + } + } +}