From bb4ec4f5422384a0655baf73aaddcbc13204373b Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Wed, 25 Jan 2023 14:45:17 +0100 Subject: [PATCH 01/64] Call push rule /actions api before the /enabled api --- .../session/pushers/UpdatePushRuleActionsTask.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt index 454b9cdd80..ec6b5d5268 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt @@ -34,10 +34,16 @@ internal interface UpdatePushRuleActionsTask : Task Date: Wed, 25 Jan 2023 14:52:30 +0100 Subject: [PATCH 02/64] Adding changelog entry --- changelog.d/8005.sdk | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/8005.sdk diff --git a/changelog.d/8005.sdk b/changelog.d/8005.sdk new file mode 100644 index 0000000000..1849776d50 --- /dev/null +++ b/changelog.d/8005.sdk @@ -0,0 +1 @@ +[Push rules] Call /actions api before /enabled api From edc04ea49d53b40192ee81c8e11c66acd965b234 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Mon, 16 Jan 2023 09:52:47 +0100 Subject: [PATCH 03/64] Adding changelog entry --- changelog.d/7864.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7864.wip diff --git a/changelog.d/7864.wip b/changelog.d/7864.wip new file mode 100644 index 0000000000..da04806b8b --- /dev/null +++ b/changelog.d/7864.wip @@ -0,0 +1 @@ +[Poll] History list: unmock data From c7f6ece825d4d6a7da1bbcbfb14fa4f296761b78 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Mon, 16 Jan 2023 10:43:35 +0100 Subject: [PATCH 04/64] Introducing a new room service for poll history --- .../android/sdk/api/session/room/Room.kt | 6 ++ .../session/room/poll/LoadedPollsStatus.kt | 25 +++++++ .../session/room/poll/PollHistoryService.kt | 55 ++++++++++++++++ .../sdk/internal/session/room/DefaultRoom.kt | 3 + .../sdk/internal/session/room/RoomFactory.kt | 3 + .../room/poll/DefaultPollHistoryService.kt | 66 +++++++++++++++++++ 6 files changed, 158 insertions(+) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/PollHistoryService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/DefaultPollHistoryService.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt index 8031fcaeea..de360c89c6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt @@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.room.model.LocalRoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.relation.RelationService import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService +import org.matrix.android.sdk.api.session.room.poll.PollHistoryService import org.matrix.android.sdk.api.session.room.read.ReadService import org.matrix.android.sdk.api.session.room.reporting.ReportingService import org.matrix.android.sdk.api.session.room.send.DraftService @@ -181,4 +182,9 @@ interface Room { * Get the LocationSharingService associated to this Room. */ fun locationSharingService(): LocationSharingService + + /** + * Get the PollHistoryService associated to this Room. + */ + fun pollHistoryService(): PollHistoryService } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt new file mode 100644 index 0000000000..d9347ff1d6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt @@ -0,0 +1,25 @@ +/* + * 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 org.matrix.android.sdk.api.session.room.poll + +/** + * Status to indicate loading of polls for a room. + */ +data class LoadedPollsStatus( + val canLoadMore: Boolean, + val nbLoadedDays: Int, +) 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 new file mode 100644 index 0000000000..e0e1477913 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/PollHistoryService.kt @@ -0,0 +1,55 @@ +/* + * 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.api.session.room.poll + +import androidx.lifecycle.LiveData +import org.matrix.android.sdk.api.session.room.model.PollResponseAggregatedSummary + +/** + * Expose methods to get history of polls in rooms. + */ +interface PollHistoryService { + + val loadingPeriodInDays: Int + + /** + * Ask to load more polls starting from last loaded polls for a period defined by + * [loadingPeriodInDays]. + */ + 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. + */ + fun getLoadedPollsStatus(): LoadedPollsStatus + + /** + * Sync polls from last loaded polls until now. + */ + suspend fun syncPolls() + + /** + * Get currently loaded list of polls. See [loadMore]. + */ + fun getPolls(): LiveData> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt index 262c111b73..3252dff0f7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt @@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.relation.RelationService import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService +import org.matrix.android.sdk.api.session.room.poll.PollHistoryService import org.matrix.android.sdk.api.session.room.read.ReadService import org.matrix.android.sdk.api.session.room.reporting.ReportingService import org.matrix.android.sdk.api.session.room.send.DraftService @@ -72,6 +73,7 @@ internal class DefaultRoom( private val roomVersionService: RoomVersionService, private val viaParameterFinder: ViaParameterFinder, private val locationSharingService: LocationSharingService, + private val pollHistoryService: PollHistoryService, override val coroutineDispatchers: MatrixCoroutineDispatchers ) : Room { @@ -116,4 +118,5 @@ internal class DefaultRoom( override fun roomAccountDataService() = roomAccountDataService override fun roomVersionService() = roomVersionService override fun locationSharingService() = locationSharingService + override fun pollHistoryService() = pollHistoryService } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt index ffe7679575..86c414863d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt @@ -28,6 +28,7 @@ import org.matrix.android.sdk.internal.session.room.draft.DefaultDraftService import org.matrix.android.sdk.internal.session.room.location.DefaultLocationSharingService import org.matrix.android.sdk.internal.session.room.membership.DefaultMembershipService import org.matrix.android.sdk.internal.session.room.notification.DefaultRoomPushRuleService +import org.matrix.android.sdk.internal.session.room.poll.DefaultPollHistoryService import org.matrix.android.sdk.internal.session.room.read.DefaultReadService import org.matrix.android.sdk.internal.session.room.relation.DefaultRelationService import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportingService @@ -71,6 +72,7 @@ internal class DefaultRoomFactory @Inject constructor( private val roomAccountDataServiceFactory: DefaultRoomAccountDataService.Factory, private val viaParameterFinder: ViaParameterFinder, private val locationSharingServiceFactory: DefaultLocationSharingService.Factory, + private val pollHistoryServiceFactory: DefaultPollHistoryService.Factory, private val coroutineDispatchers: MatrixCoroutineDispatchers ) : RoomFactory { @@ -99,6 +101,7 @@ internal class DefaultRoomFactory @Inject constructor( roomVersionService = roomVersionServiceFactory.create(roomId), viaParameterFinder = viaParameterFinder, locationSharingService = locationSharingServiceFactory.create(roomId), + pollHistoryService = pollHistoryServiceFactory.create(roomId), coroutineDispatchers = coroutineDispatchers ) } 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 new file mode 100644 index 0000000000..6779fbc101 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/DefaultPollHistoryService.kt @@ -0,0 +1,66 @@ +/* + * 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 + +import androidx.lifecycle.LiveData +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +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 timber.log.Timber + +private const val LOADING_PERIOD_IN_DAYS = 30 + +// TODO add unit tests +internal class DefaultPollHistoryService @AssistedInject constructor( + @Assisted private val roomId: String, +) : PollHistoryService { + + @AssistedFactory + interface Factory { + fun create(roomId: String): DefaultPollHistoryService + } + + init { + Timber.d("init with roomId: $roomId") + } + + override val loadingPeriodInDays: Int + get() = LOADING_PERIOD_IN_DAYS + + override suspend fun loadMore(): LoadedPollsStatus { + TODO("Not yet implemented") + } + + override fun canLoadMore(): Boolean { + TODO("Not yet implemented") + } + + override fun getLoadedPollsStatus(): LoadedPollsStatus { + TODO("Not yet implemented") + } + + override suspend fun syncPolls() { + TODO("Not yet implemented") + } + + override fun getPolls(): LiveData> { + TODO("Not yet implemented") + } +} From 1ab6faf2d25a90e95b2d6de7b81ee8ab8f2ed093 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Mon, 16 Jan 2023 14:57:51 +0100 Subject: [PATCH 05/64] Adding PollHistoryStatusEntity --- .../database/model/PollHistoryStatusEntity.kt | 69 +++++++++++++++++++ .../database/model/SessionRealmModule.kt | 1 + 2 files changed, 70 insertions(+) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt new file mode 100644 index 0000000000..4eb4d1a92a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt @@ -0,0 +1,69 @@ +/* + * 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.database.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +/** + * Keeps track of the loading process of the poll history. + */ +internal open class PollHistoryStatusEntity( + /** + * The related room id. + */ + @PrimaryKey + var roomId: String = "", + + /** + * Timestamp of the in progress poll sync target in backward direction in milliseconds. + */ + var currentTimestampTargetBackwardMs: Long? = null, + + /** + * Timestamp of the last completed poll sync target in backward direction in milliseconds. + */ + var lastTimestampTargetBackwardMs: Long? = null, + + /** + * Indicate whether all polls in a room have been synced for the current timestamp target in backward direction. + */ + var currentTimestampTargetBackwardReached: Boolean = false, + + /** + * Indicate whether all polls in a room have been synced in backward direction. + */ + var isEndOfPollsBackward: Boolean = false, + + /** + * Indicate whether at least one poll sync has been fully completed backward for the given room. + */ + var hasCompletedASyncBackward: Boolean = false, + + /** + * Token of the end of the last synced chunk in backward direction. + */ + var tokenEndBackward: String? = null, + + /** + * Token of the start of the last synced chunk in forward direction. + */ + var tokenStartForward: String? = null, +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index 93fe1bd1d2..af8dfd7ece 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -73,6 +73,7 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit UserPresenceEntity::class, ThreadSummaryEntity::class, ThreadListPageEntity::class, + PollHistoryStatusEntity::class, ] ) internal class SessionRealmModule From 9d921286311c36a3278f1b073265662ffce1117a Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Mon, 16 Jan 2023 16:07:29 +0100 Subject: [PATCH 06/64] Starting to implement LoadMorePollsTask with update of poll history status --- .../query/PollHistoryStatusEntityQueries.kt | 31 +++++++++ .../sdk/internal/session/room/RoomModule.kt | 5 ++ .../room/poll/DefaultPollHistoryService.kt | 13 +++- .../session/room/poll/LoadMorePollsTask.kt | 67 +++++++++++++++++++ .../polls/list/data/RoomPollDataSource.kt | 18 ++++- 5 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PollHistoryStatusEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/LoadMorePollsTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PollHistoryStatusEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PollHistoryStatusEntityQueries.kt new file mode 100644 index 0000000000..1396eb897b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/PollHistoryStatusEntityQueries.kt @@ -0,0 +1,31 @@ +/* + * 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.database.query + +import io.realm.Realm +import io.realm.kotlin.createObject +import io.realm.kotlin.where +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntity +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntityFields + +internal fun PollHistoryStatusEntity.Companion.get(realm: Realm, roomId: String): PollHistoryStatusEntity? { + return realm.where().equalTo(PollHistoryStatusEntityFields.ROOM_ID, roomId).findFirst() +} + +internal fun PollHistoryStatusEntity.Companion.getOrCreate(realm: Realm, roomId: String): PollHistoryStatusEntity { + return get(realm, roomId) ?: realm.createObject(roomId) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index c28d24995f..dab2d340b6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -89,6 +89,8 @@ import org.matrix.android.sdk.internal.session.room.peeking.DefaultPeekRoomTask import org.matrix.android.sdk.internal.session.room.peeking.DefaultResolveRoomStateTask import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask +import org.matrix.android.sdk.internal.session.room.poll.DefaultLoadMorePollsTask +import org.matrix.android.sdk.internal.session.room.poll.LoadMorePollsTask import org.matrix.android.sdk.internal.session.room.read.DefaultMarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.room.read.DefaultSetReadMarkersTask import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask @@ -359,4 +361,7 @@ internal abstract class RoomModule { @Binds abstract fun bindFetchPollResponseEventsTask(task: DefaultFetchPollResponseEventsTask): FetchPollResponseEventsTask + + @Binds + abstract fun bindLoadMorePollsTask(task: DefaultLoadMorePollsTask): LoadMorePollsTask } 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 6779fbc101..e476eaf144 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 @@ -23,13 +23,17 @@ import dagger.assisted.AssistedInject 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.internal.util.time.Clock import timber.log.Timber private const val LOADING_PERIOD_IN_DAYS = 30 +private const val EVENTS_PAGE_SIZE = 200 // TODO add unit tests internal class DefaultPollHistoryService @AssistedInject constructor( @Assisted private val roomId: String, + private val clock: Clock, + private val loadMorePollsTask: LoadMorePollsTask, ) : PollHistoryService { @AssistedFactory @@ -45,7 +49,14 @@ internal class DefaultPollHistoryService @AssistedInject constructor( get() = LOADING_PERIOD_IN_DAYS override suspend fun loadMore(): LoadedPollsStatus { - TODO("Not yet implemented") + // TODO when to set currentTimestampMs and who is responsible for it? + val params = LoadMorePollsTask.Params( + roomId = roomId, + currentTimestampMs = clock.epochMillis(), + loadingPeriodInDays = loadingPeriodInDays, + eventsPageSize = EVENTS_PAGE_SIZE, + ) + return loadMorePollsTask.execute(params) } override fun canLoadMore(): Boolean { 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 new file mode 100644 index 0000000000..53841ae3f5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/LoadMorePollsTask.kt @@ -0,0 +1,67 @@ +/* + * 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 + +import com.zhuinden.monarchy.Monarchy +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.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +internal interface LoadMorePollsTask : Task { + data class Params( + val roomId: String, + val currentTimestampMs: Long, + val loadingPeriodInDays: Int, + val eventsPageSize: Int, + ) +} + +private const val MILLISECONDS_PER_DAY = 24 * 60 * 60_000 + +internal class DefaultLoadMorePollsTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, +) : LoadMorePollsTask { + + override suspend fun execute(params: LoadMorePollsTask.Params): LoadedPollsStatus { + updatePollHistoryStatus(params) + + return LoadedPollsStatus( + canLoadMore = true, + nbLoadedDays = 10, + ) + } + + private suspend fun updatePollHistoryStatus(params: LoadMorePollsTask.Params) { + monarchy.awaitTransaction { realm -> + val status = PollHistoryStatusEntity.getOrCreate(realm, params.roomId) + val currentTargetTimestamp = status.currentTimestampTargetBackwardMs + val loadingPeriodMs = MILLISECONDS_PER_DAY * params.loadingPeriodInDays + if (currentTargetTimestamp == null) { + // first load, compute the target timestamp + status.currentTimestampTargetBackwardMs = params.currentTimestampMs - loadingPeriodMs + } else if (status.currentTimestampTargetBackwardReached) { + // previous load has finished, update the target timestamp + status.currentTimestampTargetBackwardMs = currentTargetTimestamp - loadingPeriodMs + status.currentTimestampTargetBackwardReached = false + } + } + } +} 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 c0efb1efa1..e4f59da0d7 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,23 +16,35 @@ 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.PollHistoryService import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton -class RoomPollDataSource @Inject constructor() { +class RoomPollDataSource @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { private val pollsFlow = MutableSharedFlow>(replay = 1) private val polls = mutableListOf() private var fakeLoadCounter = 0 + private fun getPollHistoryService(roomId: String): PollHistoryService? { + return activeSessionHolder + .getSafeActiveSession() + ?.getRoom(roomId) + ?.pollHistoryService() + } + // TODO // unmock using SDK service + add unit tests // after unmock, expose domain layer model (entity) and do the mapping to PollSummary in the UI layer @@ -54,8 +66,10 @@ class RoomPollDataSource @Inject constructor() { } suspend fun loadMorePolls(roomId: String): LoadedPollsStatus { + getPollHistoryService(roomId)?.loadMore() + // TODO - // unmock using SDK service + add unit tests + // remove mocked data + add unit tests delay(3000) fakeLoadCounter++ when (fakeLoadCounter) { From aa736e2bfcac92c93a67958eedf6c267c2601c24 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Tue, 17 Jan 2023 10:02:35 +0100 Subject: [PATCH 07/64] Set page size to 250 --- .../sdk/internal/session/room/poll/DefaultPollHistoryService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e476eaf144..4a3dff7b39 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 @@ -27,7 +27,7 @@ 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 = 200 +private const val EVENTS_PAGE_SIZE = 250 // TODO add unit tests internal class DefaultPollHistoryService @AssistedInject constructor( From 54737895773d4120a18dba1e8414b565620f3e1b Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Tue, 17 Jan 2023 16:59:07 +0100 Subject: [PATCH 08/64] Removing non necessary fields that can be computed using other existing fields --- .../database/model/PollHistoryStatusEntity.kt | 29 ++++++++++++------- .../PollResponseAggregatedSummaryEntity.kt | 5 +--- .../session/room/poll/LoadMorePollsTask.kt | 16 ++++++---- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt index 4eb4d1a92a..0f28d51ffb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt @@ -39,21 +39,11 @@ internal open class PollHistoryStatusEntity( */ var lastTimestampTargetBackwardMs: Long? = null, - /** - * Indicate whether all polls in a room have been synced for the current timestamp target in backward direction. - */ - var currentTimestampTargetBackwardReached: Boolean = false, - /** * Indicate whether all polls in a room have been synced in backward direction. */ var isEndOfPollsBackward: Boolean = false, - /** - * Indicate whether at least one poll sync has been fully completed backward for the given room. - */ - var hasCompletedASyncBackward: Boolean = false, - /** * Token of the end of the last synced chunk in backward direction. */ @@ -66,4 +56,23 @@ internal open class PollHistoryStatusEntity( ) : RealmObject() { companion object + + /** + * Indicate whether at least one poll sync has been fully completed backward for the given room. + */ + val hasCompletedASyncBackward: Boolean + get() = lastTimestampTargetBackwardMs != null + + /** + * Indicate whether all polls in a room have been synced for the current timestamp target in backward direction. + */ + val currentTimestampTargetBackwardReached: Boolean + get() = checkIfCurrentTimestampTargetBackwardIsReached() + + private fun checkIfCurrentTimestampTargetBackwardIsReached(): Boolean { + val currentTarget = currentTimestampTargetBackwardMs + val lastTarget = lastTimestampTargetBackwardMs + // last timestamp target should be older or equal to the current target + return currentTarget != null && lastTarget != null && lastTarget <= currentTarget + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt index 906e329f6f..e74f8e2ce9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt @@ -36,7 +36,4 @@ internal open class PollResponseAggregatedSummaryEntity( var sourceLocalEchoEvents: RealmList = RealmList(), // list of related event ids which are encrypted due to decryption failure var encryptedRelatedEventIds: RealmList = RealmList(), -) : RealmObject() { - - companion object -} +) : RealmObject() 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 53841ae3f5..c29983bff3 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 @@ -43,6 +43,10 @@ internal class DefaultLoadMorePollsTask @Inject constructor( override suspend fun execute(params: LoadMorePollsTask.Params): LoadedPollsStatus { 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 + // unmock and check how it behaves when cancelling the process: it should resume where it was stopped return LoadedPollsStatus( canLoadMore = true, nbLoadedDays = 10, @@ -52,15 +56,15 @@ internal class DefaultLoadMorePollsTask @Inject constructor( private suspend fun updatePollHistoryStatus(params: LoadMorePollsTask.Params) { monarchy.awaitTransaction { realm -> val status = PollHistoryStatusEntity.getOrCreate(realm, params.roomId) - val currentTargetTimestamp = status.currentTimestampTargetBackwardMs - val loadingPeriodMs = MILLISECONDS_PER_DAY * params.loadingPeriodInDays - if (currentTargetTimestamp == null) { + val currentTargetTimestampMs = status.currentTimestampTargetBackwardMs + val lastTargetTimestampMs = status.lastTimestampTargetBackwardMs + val loadingPeriodMs: Long = MILLISECONDS_PER_DAY * params.loadingPeriodInDays.toLong() + if (currentTargetTimestampMs == null) { // first load, compute the target timestamp status.currentTimestampTargetBackwardMs = params.currentTimestampMs - loadingPeriodMs - } else if (status.currentTimestampTargetBackwardReached) { + } else if (lastTargetTimestampMs != null && status.currentTimestampTargetBackwardReached) { // previous load has finished, update the target timestamp - status.currentTimestampTargetBackwardMs = currentTargetTimestamp - loadingPeriodMs - status.currentTimestampTargetBackwardReached = false + status.currentTimestampTargetBackwardMs = lastTargetTimestampMs - loadingPeriodMs } } } From 3e118f24adb94aedb04f9d9753d07e89ace95ba6 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Wed, 18 Jan 2023 14:25:40 +0100 Subject: [PATCH 09/64] Loading events in a loop --- .../session/room/poll/LoadedPollsStatus.kt | 4 +- .../session/room/poll/PollHistoryService.kt | 6 - .../database/model/PollHistoryStatusEntity.kt | 35 ++++- .../sdk/internal/session/room/RoomAPI.kt | 2 +- .../room/poll/DefaultPollHistoryService.kt | 5 - .../session/room/poll/LoadMorePollsTask.kt | 90 ++++++++++-- .../session/room/poll/PollConstants.kt | 21 +++ .../roomprofile/polls/RoomPollsViewModel.kt | 4 +- .../roomprofile/polls/RoomPollsViewState.kt | 2 +- ...adedPollsStatus.kt => PollHistoryError.kt} | 7 +- .../polls/list/data/RoomPollDataSource.kt | 129 ++---------------- .../polls/list/data/RoomPollRepository.kt | 1 + .../domain/GetLoadedPollsStatusUseCase.kt | 2 +- .../polls/list/domain/LoadMorePollsUseCase.kt | 2 +- .../polls/list/ui/RoomPollsListFragment.kt | 4 +- .../polls/RoomPollsViewModelTest.kt | 6 +- .../domain/GetLoadedPollsStatusUseCaseTest.kt | 4 +- .../list/domain/LoadMorePollsUseCaseTest.kt | 13 +- 18 files changed, 169 insertions(+), 168 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/PollConstants.kt rename vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/{LoadedPollsStatus.kt => PollHistoryError.kt} (87%) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt index d9347ff1d6..f4a7dcc6c2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt @@ -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, ) 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 e0e1477913..ad53febc50 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 @@ -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. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt index 0f28d51ffb..a1c270e56e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt @@ -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() + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index aa4bdb1dd4..cf57e90c25 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -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?, 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 4a3dff7b39..74f59b6782 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 @@ -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") } 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 c29983bff3..03b6c31fec 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 @@ -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 + 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() } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/PollConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/PollConstants.kt new file mode 100644 index 0000000000..bbc230610c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/PollConstants.kt @@ -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 +} 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 b634881f70..b72486402b 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 @@ -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, ) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt index fa985c5c76..4a5c138b6a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/RoomPollsViewState.kt @@ -25,7 +25,7 @@ data class RoomPollsViewState( val polls: List = emptyList(), val isLoadingMore: Boolean = false, val canLoadMore: Boolean = true, - val nbLoadedDays: Int = 0, + val nbSyncedDays: Int = 0, val isSyncing: Boolean = false, ) : MavericksState { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/LoadedPollsStatus.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/PollHistoryError.kt similarity index 87% rename from vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/LoadedPollsStatus.kt rename to vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/PollHistoryError.kt index c3971bb289..37b7d934bb 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/LoadedPollsStatus.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/PollHistoryError.kt @@ -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() +} 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 e4f59da0d7..72ca464951 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 @@ -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>(replay = 1) - private val polls = mutableListOf() 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> { 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 { - 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 { - 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 { - 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 { - 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) } } 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 d3577df6c1..6f9b780464 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 @@ -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( diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCase.kt index 55324b253f..2bac26f79c 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCase.kt @@ -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( diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCase.kt index df3270552d..fce222cae6 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCase.kt @@ -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( diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollsListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollsListFragment.kt index 5920eb046e..1c33959824 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollsListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/RoomPollsListFragment.kt @@ -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() diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt index efb905c97f..1cac603ae2 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt @@ -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() diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt index c87a15fb02..12c23797f0 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt @@ -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 diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt index 16405d98c3..4c769de222 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt @@ -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) } } } From 10be07590df0d4120e0e7f23dcece4efbc103edb Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Wed, 18 Jan 2023 14:53:14 +0100 Subject: [PATCH 10/64] Get loaded polls status use case --- .../session/room/poll/PollHistoryService.kt | 2 +- .../sdk/internal/session/room/RoomModule.kt | 5 ++ .../room/poll/DefaultPollHistoryService.kt | 9 +++- .../room/poll/GetLoadedPollsStatusTask.kt | 48 +++++++++++++++++++ .../roomprofile/polls/RoomPollsViewModel.kt | 19 +++----- .../polls/list/data/PollHistoryError.kt | 2 +- .../polls/list/data/RoomPollDataSource.kt | 19 ++------ .../polls/list/data/RoomPollRepository.kt | 2 +- .../domain/GetLoadedPollsStatusUseCase.kt | 2 +- .../polls/list/domain/SyncPollsUseCase.kt | 5 +- .../polls/RoomPollsViewModelTest.kt | 16 +++---- 11 files changed, 88 insertions(+), 41 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.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 ad53febc50..866c3e6b81 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 @@ -35,7 +35,7 @@ interface PollHistoryService { /** * Get the current status of the loaded polls. */ - fun getLoadedPollsStatus(): LoadedPollsStatus + suspend fun getLoadedPollsStatus(): LoadedPollsStatus /** * Sync polls from last loaded polls until now. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index dab2d340b6..17e57aa2ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -89,7 +89,9 @@ import org.matrix.android.sdk.internal.session.room.peeking.DefaultPeekRoomTask import org.matrix.android.sdk.internal.session.room.peeking.DefaultResolveRoomStateTask import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask +import org.matrix.android.sdk.internal.session.room.poll.DefaultGetLoadedPollsStatusTask import org.matrix.android.sdk.internal.session.room.poll.DefaultLoadMorePollsTask +import org.matrix.android.sdk.internal.session.room.poll.GetLoadedPollsStatusTask import org.matrix.android.sdk.internal.session.room.poll.LoadMorePollsTask import org.matrix.android.sdk.internal.session.room.read.DefaultMarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.room.read.DefaultSetReadMarkersTask @@ -364,4 +366,7 @@ internal abstract class RoomModule { @Binds abstract fun bindLoadMorePollsTask(task: DefaultLoadMorePollsTask): LoadMorePollsTask + + @Binds + abstract fun bindGetLoadedPollsStatusTask(task: DefaultGetLoadedPollsStatusTask): GetLoadedPollsStatusTask } 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 74f59b6782..c4ba89f4a6 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 @@ -34,6 +34,7 @@ internal class DefaultPollHistoryService @AssistedInject constructor( @Assisted private val roomId: String, private val clock: Clock, private val loadMorePollsTask: LoadMorePollsTask, + private val getLoadedPollsStatusTask: GetLoadedPollsStatusTask, ) : PollHistoryService { @AssistedFactory @@ -58,8 +59,12 @@ internal class DefaultPollHistoryService @AssistedInject constructor( return loadMorePollsTask.execute(params) } - override fun getLoadedPollsStatus(): LoadedPollsStatus { - TODO("Not yet implemented") + override suspend fun getLoadedPollsStatus(): LoadedPollsStatus { + val params = GetLoadedPollsStatusTask.Params( + roomId = roomId, + currentTimestampMs = clock.epochMillis(), + ) + return getLoadedPollsStatusTask.execute(params) } override suspend fun syncPolls() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.kt new file mode 100644 index 0000000000..118c81a451 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.kt @@ -0,0 +1,48 @@ +/* + * 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 + +import com.zhuinden.monarchy.Monarchy +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.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +internal interface GetLoadedPollsStatusTask : Task { + data class Params( + val roomId: String, + val currentTimestampMs: Long, + ) +} + +internal class DefaultGetLoadedPollsStatusTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, +) : GetLoadedPollsStatusTask { + + override suspend fun execute(params: GetLoadedPollsStatusTask.Params): LoadedPollsStatus { + return monarchy.awaitTransaction { realm -> + val status = PollHistoryStatusEntity.getOrCreate(realm, params.roomId) + LoadedPollsStatus( + canLoadMore = status.isEndOfPollsBackward.not(), + nbSyncedDays = status.getNbSyncedDays(params.currentTimestampMs), + ) + } + } +} 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 b72486402b..fccdef87b8 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 @@ -48,26 +48,21 @@ class RoomPollsViewModel @AssistedInject constructor( init { val roomId = initialState.roomId - updateLoadedPollStatus(roomId) syncPolls(roomId) observePolls(roomId) } - private fun updateLoadedPollStatus(roomId: String) { - val loadedPollsStatus = getLoadedPollsStatusUseCase.execute(roomId) - setState { - copy( - canLoadMore = loadedPollsStatus.canLoadMore, - nbSyncedDays = loadedPollsStatus.nbSyncedDays, - ) - } - } - private fun syncPolls(roomId: String) { viewModelScope.launch { setState { copy(isSyncing = true) } val result = runCatching { - syncPollsUseCase.execute(roomId) + val loadedPollsStatus = syncPollsUseCase.execute(roomId) + setState { + copy( + canLoadMore = loadedPollsStatus.canLoadMore, + nbSyncedDays = loadedPollsStatus.nbSyncedDays, + ) + } } if (result.isFailure) { _viewEvents.post(RoomPollsViewEvent.LoadingError) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/PollHistoryError.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/PollHistoryError.kt index 37b7d934bb..67d59faebd 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/PollHistoryError.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/data/PollHistoryError.kt @@ -17,5 +17,5 @@ package im.vector.app.features.roomprofile.polls.list.data sealed class PollHistoryError : Exception() { - object LoadingError : PollHistoryError() + object UnknownRoomError : PollHistoryError() } 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 72ca464951..f22f494f49 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 @@ -34,13 +34,13 @@ class RoomPollDataSource @Inject constructor( ) { private val pollsFlow = MutableSharedFlow>(replay = 1) - private var fakeLoadCounter = 0 - private fun getPollHistoryService(roomId: String): PollHistoryService? { + private fun getPollHistoryService(roomId: String): PollHistoryService { return activeSessionHolder .getSafeActiveSession() ?.getRoom(roomId) ?.pollHistoryService() + ?: throw PollHistoryError.UnknownRoomError } // TODO @@ -51,21 +51,12 @@ class RoomPollDataSource @Inject constructor( return pollsFlow.asSharedFlow() } - fun getLoadedPollsStatus(roomId: String): LoadedPollsStatus { - Timber.d("roomId=$roomId") - // TODO unmock using SDK - return LoadedPollsStatus( - canLoadMore = canLoadMore(), - nbSyncedDays = fakeLoadCounter * 30, - ) - } - - private fun canLoadMore(): Boolean { - return fakeLoadCounter < 2 + suspend fun getLoadedPollsStatus(roomId: String): LoadedPollsStatus { + return getPollHistoryService(roomId).getLoadedPollsStatus() } suspend fun loadMorePolls(roomId: String): LoadedPollsStatus { - return getPollHistoryService(roomId)?.loadMore() ?: throw PollHistoryError.LoadingError + return getPollHistoryService(roomId).loadMore() } suspend fun syncPolls(roomId: String) { 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 6f9b780464..4679af4434 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 @@ -30,7 +30,7 @@ class RoomPollRepository @Inject constructor( return roomPollDataSource.getPolls(roomId) } - fun getLoadedPollsStatus(roomId: String): LoadedPollsStatus { + suspend fun getLoadedPollsStatus(roomId: String): LoadedPollsStatus { return roomPollDataSource.getLoadedPollsStatus(roomId) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCase.kt index 2bac26f79c..d37e27ff03 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCase.kt @@ -24,7 +24,7 @@ class GetLoadedPollsStatusUseCase @Inject constructor( private val roomPollRepository: RoomPollRepository, ) { - fun execute(roomId: String): LoadedPollsStatus { + suspend fun execute(roomId: String): LoadedPollsStatus { return roomPollRepository.getLoadedPollsStatus(roomId) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCase.kt index b6a344f7f8..7346406c84 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCase.kt @@ -17,6 +17,7 @@ package im.vector.app.features.roomprofile.polls.list.domain import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus import javax.inject.Inject /** @@ -24,9 +25,11 @@ import javax.inject.Inject */ class SyncPollsUseCase @Inject constructor( private val roomPollRepository: RoomPollRepository, + private val getLoadedPollsStatusUseCase: GetLoadedPollsStatusUseCase, ) { - suspend fun execute(roomId: String) { + suspend fun execute(roomId: String): LoadedPollsStatus { roomPollRepository.syncPolls(roomId) + return getLoadedPollsStatusUseCase.execute(roomId) } } diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt index 1cac603ae2..adbf32006e 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt @@ -17,7 +17,6 @@ package im.vector.app.features.roomprofile.polls import com.airbnb.mvrx.test.MavericksTestRule -import im.vector.app.features.roomprofile.polls.list.data.LoadedPollsStatus 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 @@ -34,6 +33,7 @@ import io.mockk.verify import kotlinx.coroutines.flow.flowOf import org.junit.Rule import org.junit.Test +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus private const val A_ROOM_ID = "room-id" @@ -68,7 +68,7 @@ class RoomPollsViewModelTest { val expectedViewState = initialState.copy( polls = polls, canLoadMore = loadedPollsStatus.canLoadMore, - nbSyncedDays = loadedPollsStatus.nbLoadedDays, + nbSyncedDays = loadedPollsStatus.nbSyncedDays, ) // When @@ -116,7 +116,7 @@ class RoomPollsViewModelTest { val stateAfterInit = initialState.copy( polls = polls, canLoadMore = loadedPollsStatus.canLoadMore, - nbSyncedDays = loadedPollsStatus.nbLoadedDays, + nbSyncedDays = loadedPollsStatus.nbSyncedDays, ) // When @@ -128,7 +128,7 @@ class RoomPollsViewModelTest { .assertStatesChanges( stateAfterInit, { copy(isLoadingMore = true) }, - { copy(canLoadMore = newLoadedPollsStatus.canLoadMore, nbSyncedDays = newLoadedPollsStatus.nbLoadedDays) }, + { copy(canLoadMore = newLoadedPollsStatus.canLoadMore, nbSyncedDays = newLoadedPollsStatus.nbSyncedDays) }, { copy(isLoadingMore = false) }, ) .finish() @@ -148,20 +148,20 @@ class RoomPollsViewModelTest { } private fun givenLoadMoreWithSuccess(): LoadedPollsStatus { - val loadedPollsStatus = givenALoadedPollsStatus(canLoadMore = false, nbLoadedDays = 20) + val loadedPollsStatus = givenALoadedPollsStatus(canLoadMore = false, nbSyncedDays = 20) coEvery { fakeLoadMorePollsUseCase.execute(A_ROOM_ID) } returns loadedPollsStatus return loadedPollsStatus } private fun givenGetLoadedPollsStatusSuccess(): LoadedPollsStatus { val loadedPollsStatus = givenALoadedPollsStatus() - every { fakeGetLoadedPollsStatusUseCase.execute(A_ROOM_ID) } returns loadedPollsStatus + coEvery { fakeGetLoadedPollsStatusUseCase.execute(A_ROOM_ID) } returns loadedPollsStatus return loadedPollsStatus } - private fun givenALoadedPollsStatus(canLoadMore: Boolean = true, nbLoadedDays: Int = 10) = + private fun givenALoadedPollsStatus(canLoadMore: Boolean = true, nbSyncedDays: Int = 10) = LoadedPollsStatus( canLoadMore = canLoadMore, - nbLoadedDays = nbLoadedDays, + nbSyncedDays = nbSyncedDays, ) } From e3a2000e29153e1897140ae5aa88ad9d9085bc21 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Wed, 18 Jan 2023 15:03:51 +0100 Subject: [PATCH 11/64] Calling syncPolls of SDK service --- .../session/room/poll/DefaultPollHistoryService.kt | 11 ++++------- .../roomprofile/polls/list/data/RoomPollDataSource.kt | 6 +----- 2 files changed, 5 insertions(+), 12 deletions(-) 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 c4ba89f4a6..e01d91d1be 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 @@ -20,11 +20,11 @@ import androidx.lifecycle.LiveData import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.delay 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.internal.util.time.Clock -import timber.log.Timber private const val LOADING_PERIOD_IN_DAYS = 30 private const val EVENTS_PAGE_SIZE = 250 @@ -42,10 +42,6 @@ internal class DefaultPollHistoryService @AssistedInject constructor( fun create(roomId: String): DefaultPollHistoryService } - init { - Timber.d("init with roomId: $roomId") - } - override val loadingPeriodInDays: Int get() = LOADING_PERIOD_IN_DAYS @@ -68,10 +64,11 @@ internal class DefaultPollHistoryService @AssistedInject constructor( } override suspend fun syncPolls() { - TODO("Not yet implemented") + // TODO unmock + delay(1000) } override fun getPolls(): LiveData> { - TODO("Not yet implemented") + TODO("listen database and update query depending on latest PollHistoryStatusEntity.oldestTimestampReachedMs") } } 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 f22f494f49..ee3b477685 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 @@ -18,7 +18,6 @@ package im.vector.app.features.roomprofile.polls.list.data import im.vector.app.core.di.ActiveSessionHolder 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 @@ -60,9 +59,6 @@ class RoomPollDataSource @Inject constructor( } suspend fun syncPolls(roomId: String) { - Timber.d("roomId=$roomId") - // TODO unmock using SDK service - // fake sync - delay(1000) + getPollHistoryService(roomId).syncPolls() } } From 7ca532a5f6ccaedd8e916ab7820e51d3c9ee58b5 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Wed, 18 Jan 2023 15:24:09 +0100 Subject: [PATCH 12/64] Filter and store poll events --- .../sdk/internal/session/room/RoomModule.kt | 5 ++ .../room/event/FilterAndStoreEventsTask.kt | 80 +++++++++++++++++++ .../session/room/poll/LoadMorePollsTask.kt | 24 ++++-- .../poll/FetchPollResponseEventsTask.kt | 56 +++---------- 4 files changed, 114 insertions(+), 51 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 17e57aa2ee..56925d55d5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -59,6 +59,8 @@ import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDire import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask +import org.matrix.android.sdk.internal.session.room.event.DefaultFilterAndStoreEventsTask +import org.matrix.android.sdk.internal.session.room.event.FilterAndStoreEventsTask import org.matrix.android.sdk.internal.session.room.location.CheckIfExistingActiveLiveTask import org.matrix.android.sdk.internal.session.room.location.DefaultCheckIfExistingActiveLiveTask import org.matrix.android.sdk.internal.session.room.location.DefaultGetActiveBeaconInfoForUserTask @@ -369,4 +371,7 @@ internal abstract class RoomModule { @Binds abstract fun bindGetLoadedPollsStatusTask(task: DefaultGetLoadedPollsStatusTask): GetLoadedPollsStatusTask + + @Binds + abstract fun bindFilterAndStoreEventsTask(task: DefaultFilterAndStoreEventsTask): FilterAndStoreEventsTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt new file mode 100644 index 0000000000..aa836c8491 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt @@ -0,0 +1,80 @@ +/* + * 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.event + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.crypto.EventDecryptor +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock +import javax.inject.Inject + +internal interface FilterAndStoreEventsTask : Task { + data class Params( + val roomId: String, + val events: List, + val filterPredicate: (Event) -> Boolean, + ) +} + +internal class DefaultFilterAndStoreEventsTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + private val clock: Clock, + private val eventDecryptor: EventDecryptor, +) : FilterAndStoreEventsTask { + + override suspend fun execute(params: FilterAndStoreEventsTask.Params) { + val filteredEvents = params.events + .map { decryptEventIfNeeded(it) } + .filter { params.filterPredicate(it) } + + addMissingEventsInDB(params.roomId, filteredEvents) + } + + private suspend fun addMissingEventsInDB(roomId: String, events: List) { + monarchy.awaitTransaction { realm -> + val eventIdsToCheck = events.mapNotNull { it.eventId }.filter { it.isNotEmpty() } + if (eventIdsToCheck.isNotEmpty()) { + val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId } + + events.filterNot { it.eventId in existingIds } + .map { it.toEntity(roomId = roomId, sendState = SendState.SYNCED, ageLocalTs = computeLocalTs(it)) } + .forEach { it.copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) } + } + } + } + + private suspend fun decryptEventIfNeeded(event: Event): Event { + if (event.isEncrypted()) { + eventDecryptor.decryptEventAndSaveResult(event, timeline = "") + } + + event.ageLocalTs = computeLocalTs(event) + + return event + } + + private fun computeLocalTs(event: Event) = clock.epochMillis() - (event.unsignedData?.age ?: 0) +} 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 03b6c31fec..7870897ace 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 @@ -17,6 +17,8 @@ package org.matrix.android.sdk.internal.session.room.poll import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.events.model.isPoll +import org.matrix.android.sdk.api.session.events.model.isPollResponse 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 @@ -24,6 +26,7 @@ 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.event.FilterAndStoreEventsTask 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 @@ -44,6 +47,7 @@ internal class DefaultLoadMorePollsTask @Inject constructor( @SessionDatabase private val monarchy: Monarchy, private val roomAPI: RoomAPI, private val globalErrorReceiver: GlobalErrorReceiver, + private val filterAndStoreEventsTask: FilterAndStoreEventsTask, ) : LoadMorePollsTask { override suspend fun execute(params: LoadMorePollsTask.Params): LoadedPollsStatus { @@ -53,9 +57,10 @@ internal class DefaultLoadMorePollsTask @Inject constructor( currentPollHistoryStatus = fetchMorePollEventsBackward(params, currentPollHistoryStatus) } // TODO - // unmock and check how it behaves when cancelling the process: it should resume where it was stopped + // 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 + // test on large room return LoadedPollsStatus( canLoadMore = currentPollHistoryStatus.isEndOfPollsBackward.not(), @@ -89,7 +94,7 @@ internal class DefaultLoadMorePollsTask @Inject constructor( params: LoadMorePollsTask.Params, status: PollHistoryStatusEntity ): PollHistoryStatusEntity { - val chunk = executeRequest(globalErrorReceiver) { + val response = executeRequest(globalErrorReceiver) { roomAPI.getRoomMessagesFrom( roomId = params.roomId, from = status.tokenEndBackward, @@ -99,9 +104,18 @@ internal class DefaultLoadMorePollsTask @Inject constructor( ) } - // TODO decrypt events and filter in only polls to store them in local: see to mutualize with FetchPollResponseEventsTask + filterAndStorePollEvents(roomId = params.roomId, paginationResponse = response) - return updatePollHistoryStatus(roomId = params.roomId, paginationResponse = chunk) + return updatePollHistoryStatus(roomId = params.roomId, paginationResponse = response) + } + + private suspend fun filterAndStorePollEvents(roomId: String, paginationResponse: PaginationResponse) { + val filterTaskParams = FilterAndStoreEventsTask.Params( + roomId = roomId, + events = paginationResponse.events, + filterPredicate = { it.isPoll() || it.isPollResponse() } + ) + filterAndStoreEventsTask.execute(filterTaskParams) } private suspend fun updatePollHistoryStatus(roomId: String, paginationResponse: PaginationResponse): PollHistoryStatusEntity { @@ -124,7 +138,7 @@ internal class DefaultLoadMorePollsTask @Inject constructor( // 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) { + } else if (oldestEventTimestamp != null && currentTargetTimestamp != null && oldestEventTimestamp <= currentTargetTimestamp) { // target has been reached status.oldestTimestampReachedMs = oldestEventTimestamp status.tokenEndBackward = paginationResponse.end diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt index e7dd8c57eb..347c9fbf12 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/poll/FetchPollResponseEventsTask.kt @@ -17,25 +17,14 @@ package org.matrix.android.sdk.internal.session.room.relation.poll import androidx.annotation.VisibleForTesting -import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.isPollResponse -import org.matrix.android.sdk.api.session.room.send.SendState -import org.matrix.android.sdk.internal.crypto.EventDecryptor -import org.matrix.android.sdk.internal.database.mapper.toEntity -import org.matrix.android.sdk.internal.database.model.EventEntity -import org.matrix.android.sdk.internal.database.model.EventInsertType -import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore -import org.matrix.android.sdk.internal.database.query.where -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.event.FilterAndStoreEventsTask import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse import org.matrix.android.sdk.internal.task.Task -import org.matrix.android.sdk.internal.util.awaitTransaction -import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject @VisibleForTesting @@ -54,10 +43,9 @@ internal interface FetchPollResponseEventsTask : Task = runCatching { var nextBatch: String? = fetchAndProcessRelatedEventsFrom(params) @@ -70,11 +58,12 @@ internal class DefaultFetchPollResponseEventsTask @Inject constructor( private suspend fun fetchAndProcessRelatedEventsFrom(params: FetchPollResponseEventsTask.Params, from: String? = null): String? { val response = getRelatedEvents(params, from) - val filteredEvents = response.chunks - .map { decryptEventIfNeeded(it) } - .filter { it.isPollResponse() } - - addMissingEventsInDB(params.roomId, filteredEvents) + val filterTaskParams = FilterAndStoreEventsTask.Params( + roomId = params.roomId, + events = response.chunks, + filterPredicate = { it.isPollResponse() } + ) + filterAndStoreEventsTask.execute(filterTaskParams) return response.nextBatch } @@ -90,29 +79,4 @@ internal class DefaultFetchPollResponseEventsTask @Inject constructor( ) } } - - private suspend fun addMissingEventsInDB(roomId: String, events: List) { - monarchy.awaitTransaction { realm -> - val eventIdsToCheck = events.mapNotNull { it.eventId }.filter { it.isNotEmpty() } - if (eventIdsToCheck.isNotEmpty()) { - val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId } - - events.filterNot { it.eventId in existingIds } - .map { it.toEntity(roomId = roomId, sendState = SendState.SYNCED, ageLocalTs = computeLocalTs(it)) } - .forEach { it.copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) } - } - } - } - - private suspend fun decryptEventIfNeeded(event: Event): Event { - if (event.isEncrypted()) { - eventDecryptor.decryptEventAndSaveResult(event, timeline = "") - } - - event.ageLocalTs = computeLocalTs(event) - - return event - } - - private fun computeLocalTs(event: Event) = clock.epochMillis() - (event.unsignedData?.age ?: 0) } 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 13/64] 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, + ) + } + } +} From 91904a3e8f099141ef834623040bec56650cbb35 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Thu, 19 Jan 2023 17:22:37 +0100 Subject: [PATCH 14/64] Create a dedicated factory for PollOptionViewState --- .../factory/PollItemViewStateFactory.kt | 53 ++---------- .../factory/PollOptionViewStateFactory.kt | 84 +++++++++++++++++++ .../polls/list/ui/PollSummaryMapper.kt | 18 +--- 3 files changed, 95 insertions(+), 60 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollOptionViewStateFactory.kt 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 e4f0fc3ba5..3c1a1cfd85 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 @@ -19,7 +19,6 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState import im.vector.app.features.home.room.detail.timeline.item.PollResponseData import im.vector.app.features.poll.PollViewState import org.matrix.android.sdk.api.extensions.orFalse @@ -29,6 +28,7 @@ import javax.inject.Inject class PollItemViewStateFactory @Inject constructor( private val stringProvider: StringProvider, + private val pollOptionViewStateFactory: PollOptionViewStateFactory, ) { fun create( @@ -40,7 +40,6 @@ class PollItemViewStateFactory @Inject constructor( val question = pollCreationInfo?.question?.getBestQuestion().orEmpty() val pollResponseSummary = informationData.pollResponseAggregatedSummary - val winnerVoteCount = pollResponseSummary?.winnerVoteCount val totalVotes = pollResponseSummary?.totalVotes ?: 0 return when { @@ -48,7 +47,7 @@ class PollItemViewStateFactory @Inject constructor( createSendingPollViewState(question, pollCreationInfo) } informationData.pollResponseAggregatedSummary?.isClosed.orFalse() -> { - createEndedPollViewState(question, pollCreationInfo, pollResponseSummary, totalVotes, winnerVoteCount) + createEndedPollViewState(question, pollCreationInfo, pollResponseSummary, totalVotes) } pollContent.getBestPollCreationInfo()?.isUndisclosed().orFalse() -> { createUndisclosedPollViewState(question, pollCreationInfo, pollResponseSummary) @@ -67,12 +66,7 @@ class PollItemViewStateFactory @Inject constructor( question = question, votesStatus = stringProvider.getString(R.string.poll_no_votes_cast), canVote = false, - optionViewStates = pollCreationInfo?.answers?.map { answer -> - PollOptionViewState.PollSending( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "" - ) - }, + optionViewStates = pollOptionViewStateFactory.createPollSendingOptions(pollCreationInfo), ) } @@ -81,7 +75,6 @@ class PollItemViewStateFactory @Inject constructor( pollCreationInfo: PollCreationInfo?, pollResponseSummary: PollResponseData?, totalVotes: Int, - winnerVoteCount: Int?, ): PollViewState { val totalVotesText = if (pollResponseSummary?.hasEncryptedRelatedEvents.orFalse()) { stringProvider.getString(R.string.unable_to_decrypt_some_events_in_poll) @@ -92,17 +85,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( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "", - voteCount = voteSummary?.total ?: 0, - votePercentage = voteSummary?.percentage ?: 0.0, - isWinner = winnerVoteCount != 0 && voteSummary?.total == winnerVoteCount - ) - }, + optionViewStates = pollOptionViewStateFactory.createPollEndedOptions(pollCreationInfo, pollResponseSummary), ) } @@ -115,14 +98,7 @@ class PollItemViewStateFactory @Inject constructor( question = question, votesStatus = stringProvider.getString(R.string.poll_undisclosed_not_ended), canVote = true, - optionViewStates = pollCreationInfo?.answers?.map { answer -> - val isMyVote = pollResponseSummary?.myVote == answer.id - PollOptionViewState.PollUndisclosed( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "", - isSelected = isMyVote - ) - }, + optionViewStates = pollOptionViewStateFactory.createPollUndisclosedOptions(pollCreationInfo, pollResponseSummary), ) } @@ -141,17 +117,7 @@ class PollItemViewStateFactory @Inject constructor( question = question, votesStatus = totalVotesText, canVote = true, - optionViewStates = pollCreationInfo?.answers?.map { answer -> - val isMyVote = pollResponseSummary?.myVote == answer.id - val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "") - PollOptionViewState.PollVoted( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "", - voteCount = voteSummary?.total ?: 0, - votePercentage = voteSummary?.percentage ?: 0.0, - isSelected = isMyVote - ) - }, + optionViewStates = pollOptionViewStateFactory.createPollVotedOptions(pollCreationInfo, pollResponseSummary), ) } @@ -169,12 +135,7 @@ class PollItemViewStateFactory @Inject constructor( question = question, votesStatus = totalVotesText, canVote = true, - optionViewStates = pollCreationInfo?.answers?.map { answer -> - PollOptionViewState.PollReady( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "" - ) - }, + optionViewStates = pollOptionViewStateFactory.createPollReadyOptions(pollCreationInfo), ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollOptionViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollOptionViewStateFactory.kt new file mode 100644 index 0000000000..3000164f74 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollOptionViewStateFactory.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.home.room.detail.timeline.factory + +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.PollCreationInfo +import javax.inject.Inject + +// TODO add unit tests +class PollOptionViewStateFactory @Inject constructor() { + + fun createPollEndedOptions(pollCreationInfo: PollCreationInfo?, pollResponseData: PollResponseData?): List { + val winnerVoteCount = pollResponseData?.winnerVoteCount + + return pollCreationInfo?.answers?.map { answer -> + val voteSummary = pollResponseData?.getVoteSummaryOfAnOption(answer.id ?: "") + PollOptionViewState.PollEnded( + optionId = answer.id.orEmpty(), + optionAnswer = answer.getBestAnswer().orEmpty(), + voteCount = voteSummary?.total ?: 0, + votePercentage = voteSummary?.percentage ?: 0.0, + isWinner = winnerVoteCount != 0 && voteSummary?.total == winnerVoteCount + ) + } ?: emptyList() + } + + fun createPollSendingOptions(pollCreationInfo: PollCreationInfo?): List { + return pollCreationInfo?.answers?.map { answer -> + PollOptionViewState.PollSending( + optionId = answer.id.orEmpty(), + optionAnswer = answer.getBestAnswer().orEmpty(), + ) + } ?: emptyList() + } + + fun createPollUndisclosedOptions(pollCreationInfo: PollCreationInfo?, pollResponseData: PollResponseData?): List { + return pollCreationInfo?.answers?.map { answer -> + val isMyVote = pollResponseData?.myVote == answer.id + PollOptionViewState.PollUndisclosed( + optionId = answer.id.orEmpty(), + optionAnswer = answer.getBestAnswer().orEmpty(), + isSelected = isMyVote + ) + } ?: emptyList() + } + + fun createPollVotedOptions(pollCreationInfo: PollCreationInfo?, pollResponseData: PollResponseData?): List { + return pollCreationInfo?.answers?.map { answer -> + val isMyVote = pollResponseData?.myVote == answer.id + val voteSummary = pollResponseData?.getVoteSummaryOfAnOption(answer.id ?: "") + PollOptionViewState.PollVoted( + optionId = answer.id.orEmpty(), + optionAnswer = answer.getBestAnswer().orEmpty(), + voteCount = voteSummary?.total ?: 0, + votePercentage = voteSummary?.percentage ?: 0.0, + isSelected = isMyVote + ) + } ?: emptyList() + } + + fun createPollReadyOptions(pollCreationInfo: PollCreationInfo?): List { + return pollCreationInfo?.answers?.map { answer -> + PollOptionViewState.PollReady( + optionId = answer.id ?: "", + optionAnswer = answer.getBestAnswer() ?: "" + ) + } ?: emptyList() + } +} 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 index 821620e842..f16e41e944 100644 --- 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 @@ -17,8 +17,8 @@ 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.factory.PollOptionViewStateFactory 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 @@ -27,6 +27,7 @@ import javax.inject.Inject // TODO add unit tests class PollSummaryMapper @Inject constructor( private val pollResponseDataFactory: PollResponseDataFactory, + private val pollOptionViewStateFactory: PollOptionViewStateFactory, ) { fun map(timelineEvent: TimelineEvent): PollSummary { @@ -42,7 +43,7 @@ class PollSummaryMapper @Inject constructor( pollResponseData = pollResponseData ) } else { - throw IllegalStateException("expected MessagePollContent") + throw IllegalStateException("missing mandatory info about poll event with id=$eventId") } } @@ -55,23 +56,12 @@ class PollSummaryMapper @Inject constructor( 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() + winnerOptions = pollOptionViewStateFactory.createPollEndedOptions(pollCreationInfo, pollResponseData) ) } else { PollSummary.ActivePoll( From 3ba2c47d1eab7bec68aa04dfda007e97ad51efbb Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Thu, 19 Jan 2023 17:30:06 +0100 Subject: [PATCH 15/64] Load more poll during sync if there is no completed backward load --- .../sdk/api/session/room/poll/LoadedPollsStatus.kt | 1 + .../session/room/poll/GetLoadedPollsStatusTask.kt | 1 + .../internal/session/room/poll/LoadMorePollsTask.kt | 1 + .../roomprofile/polls/list/domain/SyncPollsUseCase.kt | 11 +++++++++-- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt index f4a7dcc6c2..efc01e2cdf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt @@ -22,4 +22,5 @@ package org.matrix.android.sdk.api.session.room.poll data class LoadedPollsStatus( val canLoadMore: Boolean, val nbSyncedDays: Int, + val hasCompletedASyncBackward: Boolean, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.kt index 118c81a451..98b1e5931a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.kt @@ -42,6 +42,7 @@ internal class DefaultGetLoadedPollsStatusTask @Inject constructor( LoadedPollsStatus( canLoadMore = status.isEndOfPollsBackward.not(), nbSyncedDays = status.getNbSyncedDays(params.currentTimestampMs), + hasCompletedASyncBackward = status.hasCompletedASyncBackward, ) } } 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 94b73ff211..0858d2ae91 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 @@ -65,6 +65,7 @@ internal class DefaultLoadMorePollsTask @Inject constructor( return LoadedPollsStatus( canLoadMore = currentPollHistoryStatus.isEndOfPollsBackward.not(), nbSyncedDays = currentPollHistoryStatus.getNbSyncedDays(params.currentTimestampMs), + hasCompletedASyncBackward = currentPollHistoryStatus.hasCompletedASyncBackward, ) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCase.kt index 7346406c84..7d58fb7694 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCase.kt @@ -21,15 +21,22 @@ import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus import javax.inject.Inject /** - * Sync the polls of a given room from last manual loading (see LoadMorePollsUseCase) until now. + * Sync the polls of a given room from last manual loading if any (see LoadMorePollsUseCase) until now. + * Resume or start loading more to have at least a complete load. */ class SyncPollsUseCase @Inject constructor( private val roomPollRepository: RoomPollRepository, private val getLoadedPollsStatusUseCase: GetLoadedPollsStatusUseCase, + private val loadMorePollsUseCase: LoadMorePollsUseCase, ) { suspend fun execute(roomId: String): LoadedPollsStatus { roomPollRepository.syncPolls(roomId) - return getLoadedPollsStatusUseCase.execute(roomId) + val loadedStatus = getLoadedPollsStatusUseCase.execute(roomId) + return if (loadedStatus.hasCompletedASyncBackward) { + loadedStatus + } else { + loadMorePollsUseCase.execute(roomId) + } } } From 6b6dea0c457ae0dd5a26bf4b20a6158ddf17dbb8 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Thu, 19 Jan 2023 17:48:19 +0100 Subject: [PATCH 16/64] Store in DB events which failed to be decrypted --- .../internal/session/room/event/FilterAndStoreEventsTask.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt index aa836c8491..e6e169b9b4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.event import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.crypto.EventDecryptor import org.matrix.android.sdk.internal.database.mapper.toEntity @@ -48,7 +49,9 @@ internal class DefaultFilterAndStoreEventsTask @Inject constructor( override suspend fun execute(params: FilterAndStoreEventsTask.Params) { val filteredEvents = params.events .map { decryptEventIfNeeded(it) } - .filter { params.filterPredicate(it) } + // we also filter in the encrypted events since it means there was decryption error for them + // and they may be decrypted later + .filter { params.filterPredicate(it) || it.getClearType() == EventType.ENCRYPTED } addMissingEventsInDB(params.roomId, filteredEvents) } From a3077dfaa783b46b5f677a37b527356860c46552 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Fri, 20 Jan 2023 09:46:13 +0100 Subject: [PATCH 17/64] Fix mapping to PollSummary: case of poll without any votes --- .../roomprofile/polls/RoomPollsViewModel.kt | 3 ++- .../roomprofile/polls/list/ui/PollSummaryMapper.kt | 14 ++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) 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 83b71878cd..4b1a79f561 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 @@ -29,6 +29,7 @@ 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.mapNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -74,7 +75,7 @@ class RoomPollsViewModel @AssistedInject constructor( private fun observePolls(roomId: String) { getPollsUseCase.execute(roomId) - .map { it.map { event -> pollSummaryMapper.map(event) } } + .map { it.mapNotNull { 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/ui/PollSummaryMapper.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummaryMapper.kt index f16e41e944..0bd7cd33af 100644 --- 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 @@ -22,6 +22,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.PollResponseDataF 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 timber.log.Timber import javax.inject.Inject // TODO add unit tests @@ -30,20 +31,21 @@ class PollSummaryMapper @Inject constructor( private val pollOptionViewStateFactory: PollOptionViewStateFactory, ) { - fun map(timelineEvent: TimelineEvent): PollSummary { + 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( + return if (eventId.isNotEmpty() && creationTimestamp > 0 && content is MessagePollContent) { + convertToPollSummary( eventId = eventId, creationTimestamp = creationTimestamp, messagePollContent = content, pollResponseData = pollResponseData ) } else { - throw IllegalStateException("missing mandatory info about poll event with id=$eventId") + Timber.w("missing mandatory info about poll event with id=$eventId") + null } } @@ -51,11 +53,11 @@ class PollSummaryMapper @Inject constructor( eventId: String, creationTimestamp: Long, messagePollContent: MessagePollContent, - pollResponseData: PollResponseData + pollResponseData: PollResponseData? ): PollSummary { val pollCreationInfo = messagePollContent.getBestPollCreationInfo() val pollTitle = pollCreationInfo?.question?.getBestQuestion().orEmpty() - return if (pollResponseData.isClosed) { + return if (pollResponseData?.isClosed == true) { PollSummary.EndedPoll( id = eventId, creationTimestamp = creationTimestamp, From 4cfd6d29fcb8e8dd86b49376ee24f424f536b7ba Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Fri, 20 Jan 2023 15:29:39 +0100 Subject: [PATCH 18/64] Fix query on poll events for encrypted rooms --- .../room/poll/DefaultPollHistoryService.kt | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) 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 add6dd4b90..0d4e54f114 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 @@ -25,15 +25,12 @@ 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 @@ -90,19 +87,26 @@ internal class DefaultPollHistoryService @AssistedInject constructor( return Transformations.switchMap(pollHistoryStatusLiveData) { results -> val oldestTimestamp = results.firstOrNull()?.oldestTimestampReachedMs ?: clock.epochMillis() Timber.d("oldestTimestamp=$oldestTimestamp") + getPollStartEventsAfter(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 getPollStartEventsAfter(timestampMs: Long): LiveData> { + val eventsLiveData = monarchy.findAllMappedWithChanges( + { realm -> + val pollTypes = (EventType.POLL_START.values + EventType.ENCRYPTED).toTypedArray() + realm.where() + .equalTo(TimelineEventEntityFields.ROOM_ID, roomId) + .`in`(TimelineEventEntityFields.ROOT.TYPE, pollTypes) + .greaterThan(TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS, timestampMs) + }, + { result -> + timelineEventMapper.map(result, buildReadReceipts = false) + } + ) + + return Transformations.map(eventsLiveData) { events -> + events.filter { it.root.getClearType() in EventType.POLL_START.values } } } From 492b8a012ddd1ef9c0d5cd8dbe33c59c25c8621a Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Fri, 20 Jan 2023 17:38:55 +0100 Subject: [PATCH 19/64] Use Timeline interface to paginate --- .../session/room/poll/PollHistoryService.kt | 6 ++ .../database/model/PollHistoryStatusEntity.kt | 36 +++---- .../sdk/internal/session/room/RoomFactory.kt | 5 +- .../room/poll/DefaultPollHistoryService.kt | 25 ++++- .../session/room/poll/LoadMorePollsTask.kt | 99 +++++++++---------- .../roomprofile/polls/RoomPollsViewModel.kt | 8 +- .../polls/list/data/RoomPollDataSource.kt | 4 + .../polls/list/data/RoomPollRepository.kt | 4 + .../list/domain/DisposePollHistoryUseCase.kt | 32 ++++++ 9 files changed, 143 insertions(+), 76 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCase.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 bd0d6f4ab5..b62f5a1969 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 @@ -26,6 +26,12 @@ interface PollHistoryService { val loadingPeriodInDays: Int + /** + * This must be called when you don't need the service anymore. + * It ensures the underlying database get closed. + */ + fun dispose() + /** * Ask to load more polls starting from last loaded polls for a period defined by * [loadingPeriodInDays]. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt index a1c270e56e..35075ffa0e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollHistoryStatusEntity.kt @@ -36,24 +36,24 @@ internal open class PollHistoryStatusEntity( var currentTimestampTargetBackwardMs: Long? = null, /** - * Timestamp of the oldest event synced in milliseconds. + * Timestamp of the oldest event synced once target has been reached in milliseconds. */ - var oldestTimestampReachedMs: Long? = null, + var oldestTimestampTargetReachedMs: Long? = null, + + /** + * Id of the oldest event synced. + */ + var oldestEventIdReached: String? = null, + + /** + * Id of the most recent event synced. + */ + var mostRecentEventIdReached: String? = null, /** * Indicate whether all polls in a room have been synced in backward direction. */ var isEndOfPollsBackward: Boolean = false, - - /** - * Token of the end of the last synced chunk in backward direction. - */ - var tokenEndBackward: String? = null, - - /** - * Token of the start of the last synced chunk in forward direction. - */ - var tokenStartForward: String? = null, ) : RealmObject() { companion object @@ -65,10 +65,10 @@ internal open class PollHistoryStatusEntity( return PollHistoryStatusEntity( roomId = roomId, currentTimestampTargetBackwardMs = currentTimestampTargetBackwardMs, - oldestTimestampReachedMs = oldestTimestampReachedMs, + oldestTimestampTargetReachedMs = oldestTimestampTargetReachedMs, + oldestEventIdReached = oldestEventIdReached, + mostRecentEventIdReached = mostRecentEventIdReached, isEndOfPollsBackward = isEndOfPollsBackward, - tokenEndBackward = tokenEndBackward, - tokenStartForward = tokenStartForward, ) } @@ -76,7 +76,7 @@ internal open class PollHistoryStatusEntity( * Indicate whether at least one poll sync has been fully completed backward for the given room. */ val hasCompletedASyncBackward: Boolean - get() = oldestTimestampReachedMs != null + get() = oldestTimestampTargetReachedMs != null /** * Indicate whether all polls in a room have been synced for the current timestamp target in backward direction. @@ -86,7 +86,7 @@ internal open class PollHistoryStatusEntity( private fun checkIfCurrentTimestampTargetBackwardIsReached(): Boolean { val currentTarget = currentTimestampTargetBackwardMs - val lastTarget = oldestTimestampReachedMs + val lastTarget = oldestTimestampTargetReachedMs // last timestamp target should be older or equal to the current target return currentTarget != null && lastTarget != null && lastTarget <= currentTarget } @@ -95,7 +95,7 @@ internal open class PollHistoryStatusEntity( * Compute the number of days of history currently synced. */ fun getNbSyncedDays(currentMs: Long): Int { - val oldestTimestamp = oldestTimestampReachedMs + val oldestTimestamp = oldestTimestampTargetReachedMs return if (oldestTimestamp == null) { 0 } else { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt index 86c414863d..a3fa11dedb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt @@ -77,11 +77,12 @@ internal class DefaultRoomFactory @Inject constructor( ) : RoomFactory { override fun create(roomId: String): Room { + val timelineService = timelineServiceFactory.create(roomId) return DefaultRoom( roomId = roomId, roomSummaryDataSource = roomSummaryDataSource, roomCryptoService = roomCryptoServiceFactory.create(roomId), - timelineService = timelineServiceFactory.create(roomId), + timelineService = timelineService, threadsService = threadsServiceFactory.create(roomId), threadsLocalService = threadsLocalServiceFactory.create(roomId), sendService = sendServiceFactory.create(roomId), @@ -101,7 +102,7 @@ internal class DefaultRoomFactory @Inject constructor( roomVersionService = roomVersionServiceFactory.create(roomId), viaParameterFinder = viaParameterFinder, locationSharingService = locationSharingServiceFactory.create(roomId), - pollHistoryService = pollHistoryServiceFactory.create(roomId), + pollHistoryService = pollHistoryServiceFactory.create(roomId, timelineService), coroutineDispatchers = coroutineDispatchers ) } 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 0d4e54f114..0f57b81cb8 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 @@ -28,6 +28,8 @@ import org.matrix.android.sdk.api.session.events.model.EventType 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.api.session.room.timeline.TimelineService +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings 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 @@ -43,6 +45,7 @@ private const val EVENTS_PAGE_SIZE = 250 // TODO add unit tests internal class DefaultPollHistoryService @AssistedInject constructor( @Assisted private val roomId: String, + @Assisted private val timelineService: TimelineService, @SessionDatabase private val monarchy: Monarchy, private val clock: Clock, private val loadMorePollsTask: LoadMorePollsTask, @@ -52,14 +55,30 @@ internal class DefaultPollHistoryService @AssistedInject constructor( @AssistedFactory interface Factory { - fun create(roomId: String): DefaultPollHistoryService + fun create(roomId: String, timelineService: TimelineService): DefaultPollHistoryService } override val loadingPeriodInDays: Int get() = LOADING_PERIOD_IN_DAYS + private val timeline by lazy { + // TODO check if we need to add a way to avoid using the current filter in rooms + val settings = TimelineSettings( + initialSize = EVENTS_PAGE_SIZE, + buildReadReceipts = false, + rootThreadEventId = null, + useLiveSenderInfo = false, + ) + timelineService.createTimeline(eventId = null, settings = settings).also { it.start() } + } + + override fun dispose() { + timeline.dispose() + } + override suspend fun loadMore(): LoadedPollsStatus { val params = LoadMorePollsTask.Params( + timeline = timeline, roomId = roomId, currentTimestampMs = clock.epochMillis(), loadingPeriodInDays = loadingPeriodInDays, @@ -78,6 +97,8 @@ internal class DefaultPollHistoryService @AssistedInject constructor( override suspend fun syncPolls() { // TODO unmock + // TODO when sync forward, jump to most recent event Id + paginate forward + jump to oldest eventId after + // TODO avoid possibility to call sync and loadMore at the same time from the service API, how? delay(1000) } @@ -85,7 +106,7 @@ internal class DefaultPollHistoryService @AssistedInject constructor( val pollHistoryStatusLiveData = getPollHistoryStatus() return Transformations.switchMap(pollHistoryStatusLiveData) { results -> - val oldestTimestamp = results.firstOrNull()?.oldestTimestampReachedMs ?: clock.epochMillis() + val oldestTimestamp = results.firstOrNull()?.oldestTimestampTargetReachedMs ?: clock.epochMillis() Timber.d("oldestTimestamp=$oldestTimestamp") getPollStartEventsAfter(oldestTimestamp) } 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 0858d2ae91..f22447f19c 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 @@ -17,25 +17,20 @@ package org.matrix.android.sdk.internal.session.room.poll import com.zhuinden.monarchy.Monarchy -import org.matrix.android.sdk.api.session.events.model.isPoll -import org.matrix.android.sdk.api.session.events.model.isPollResponse import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent 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.event.FilterAndStoreEventsTask 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 internal interface LoadMorePollsTask : Task { data class Params( + val timeline: Timeline, val roomId: String, val currentTimestampMs: Long, val loadingPeriodInDays: Int, @@ -45,16 +40,15 @@ internal interface LoadMorePollsTask : Task val status = PollHistoryStatusEntity.getOrCreate(realm, params.roomId) val currentTargetTimestampMs = status.currentTimestampTargetBackwardMs - val lastTargetTimestampMs = status.oldestTimestampReachedMs + val lastTargetTimestampMs = status.oldestTimestampTargetReachedMs val loadingPeriodMs: Long = MILLISECONDS_PER_DAY * params.loadingPeriodInDays.toLong() if (currentTargetTimestampMs == null) { // first load, compute the target timestamp @@ -91,62 +85,61 @@ internal class DefaultLoadMorePollsTask @Inject constructor( } } - private suspend fun fetchMorePollEventsBackward( - params: LoadMorePollsTask.Params, - status: PollHistoryStatusEntity - ): PollHistoryStatusEntity { - val response = executeRequest(globalErrorReceiver) { - roomAPI.getRoomMessagesFrom( - roomId = params.roomId, - from = status.tokenEndBackward, - dir = PaginationDirection.BACKWARDS.value, - limit = params.eventsPageSize, - filter = null - ) - } - - filterAndStorePollEvents(roomId = params.roomId, paginationResponse = response) - - return updatePollHistoryStatus(roomId = params.roomId, paginationResponse = response) - } - - private suspend fun filterAndStorePollEvents(roomId: String, paginationResponse: PaginationResponse) { - val filterTaskParams = FilterAndStoreEventsTask.Params( - roomId = roomId, - events = paginationResponse.events, - filterPredicate = { it.isPoll() || it.isPollResponse() } + private suspend fun fetchMorePollEventsBackward(params: LoadMorePollsTask.Params): PollHistoryStatusEntity { + val events = params.timeline.awaitPaginate( + direction = Timeline.Direction.BACKWARDS, + count = params.eventsPageSize, + ) + + val paginationState = params.timeline.getPaginationState(direction = Timeline.Direction.BACKWARDS) + + return updatePollHistoryStatus( + roomId = params.roomId, + events = events, + paginationState = paginationState, ) - filterAndStoreEventsTask.execute(filterTaskParams) } - private suspend fun updatePollHistoryStatus(roomId: String, paginationResponse: PaginationResponse): PollHistoryStatusEntity { + private suspend fun updatePollHistoryStatus( + roomId: String, + events: List, + paginationState: Timeline.PaginationState, + ): PollHistoryStatusEntity { return monarchy.awaitTransaction { realm -> val status = PollHistoryStatusEntity.getOrCreate(realm, roomId) - val tokenStartForward = status.tokenStartForward + val mostRecentEventIdReached = status.mostRecentEventIdReached - if (tokenStartForward == null) { - // save the start token for next forward call - status.tokenEndBackward = paginationResponse.start + if (mostRecentEventIdReached == null) { + // save it for next forward pagination + val mostRecentEvent = events + .maxByOrNull { it.root.originServerTs ?: Long.MIN_VALUE } + ?.root + status.mostRecentEventIdReached = mostRecentEvent?.eventId } - val oldestEventTimestamp = paginationResponse.events - .minByOrNull { it.originServerTs ?: Long.MAX_VALUE } - ?.originServerTs + val oldestEvent = events + .minByOrNull { it.root.originServerTs ?: Long.MAX_VALUE } + ?.root + val oldestEventTimestamp = oldestEvent?.originServerTs + val oldestEventId = oldestEvent?.eventId val currentTargetTimestamp = status.currentTimestampTargetBackwardMs - if (paginationResponse.end == null) { + if (paginationState.hasMoreToLoad.not()) { // start of the timeline is reached, there are no more events status.isEndOfPollsBackward = true - if(oldestEventTimestamp != null && oldestEventTimestamp > 0) { - status.oldestTimestampReachedMs = oldestEventTimestamp + + if (oldestEventTimestamp != null && oldestEventTimestamp > 0) { + status.oldestTimestampTargetReachedMs = 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 + status.oldestTimestampTargetReachedMs = oldestEventTimestamp + } + + if(oldestEventId != null) { + // save it for next backward pagination + status.oldestEventIdReached = oldestEventId } // return a copy of the Realm object 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 4b1a79f561..488660f7d3 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,13 +23,13 @@ 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.DisposePollHistoryUseCase 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.mapNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -38,6 +38,7 @@ class RoomPollsViewModel @AssistedInject constructor( private val getPollsUseCase: GetPollsUseCase, private val loadMorePollsUseCase: LoadMorePollsUseCase, private val syncPollsUseCase: SyncPollsUseCase, + private val disposePollHistoryUseCase: DisposePollHistoryUseCase, private val pollSummaryMapper: PollSummaryMapper, ) : VectorViewModel(initialState) { @@ -54,6 +55,11 @@ class RoomPollsViewModel @AssistedInject constructor( observePolls(roomId) } + override fun onCleared() { + withState { disposePollHistoryUseCase.execute(it.roomId) } + super.onCleared() + } + private fun syncPolls(roomId: String) { viewModelScope.launch { setState { copy(isSyncing = true) } 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 86f77afc29..60b53a90b4 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 @@ -38,6 +38,10 @@ class RoomPollDataSource @Inject constructor( ?: throw PollHistoryError.UnknownRoomError } + fun dispose(roomId: String) { + getPollHistoryService(roomId).dispose() + } + fun getPolls(roomId: String): Flow> { return getPollHistoryService(roomId).getPollEvents().asFlow() } 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 ff29ffbdc0..d993302fb7 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 @@ -25,6 +25,10 @@ class RoomPollRepository @Inject constructor( private val roomPollDataSource: RoomPollDataSource, ) { + fun dispose(roomId: String) { + roomPollDataSource.dispose(roomId) + } + fun getPolls(roomId: String): Flow> { return roomPollDataSource.getPolls(roomId) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCase.kt new file mode 100644 index 0000000000..21814b5a2b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCase.kt @@ -0,0 +1,32 @@ +/* + * 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.domain + +import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository +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 DisposePollHistoryUseCase @Inject constructor( + private val roomPollRepository: RoomPollRepository, +) { + + fun execute(roomId: String) { + roomPollRepository.dispose(roomId) + } +} From 05c4de6c6c6993112b92f695a30ffa1954ccacee Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Mon, 23 Jan 2023 10:40:28 +0100 Subject: [PATCH 20/64] Adding distinctBy on event ids for polls --- .../sdk/internal/session/room/poll/DefaultPollHistoryService.kt | 1 + 1 file changed, 1 insertion(+) 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 0f57b81cb8..2fb63ef90e 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 @@ -128,6 +128,7 @@ internal class DefaultPollHistoryService @AssistedInject constructor( return Transformations.map(eventsLiveData) { events -> events.filter { it.root.getClearType() in EventType.POLL_START.values } + .distinctBy { it.eventId } } } From 073eda75a2171cbb0d8c312d648c4a65d0083f3e Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Mon, 23 Jan 2023 11:34:50 +0100 Subject: [PATCH 21/64] Catch error during mapping from domain to UI model --- .../polls/list/ui/PollSummaryMapper.kt | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) 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 index 0bd7cd33af..9d4128e0ac 100644 --- 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 @@ -32,21 +32,28 @@ class PollSummaryMapper @Inject constructor( ) { 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 - return if (eventId.isNotEmpty() && creationTimestamp > 0 && content is MessagePollContent) { - convertToPollSummary( - eventId = eventId, - creationTimestamp = creationTimestamp, - messagePollContent = content, - pollResponseData = pollResponseData - ) - } else { - Timber.w("missing mandatory info about poll event with id=$eventId") - null + val result = runCatching { + val content = timelineEvent.getVectorLastMessageContent() + val pollResponseData = pollResponseDataFactory.create(timelineEvent) + val creationTimestamp = timelineEvent.root.originServerTs ?: 0 + return if (eventId.isNotEmpty() && creationTimestamp > 0 && content is MessagePollContent) { + convertToPollSummary( + eventId = eventId, + creationTimestamp = creationTimestamp, + messagePollContent = content, + pollResponseData = pollResponseData + ) + } else { + Timber.w("missing mandatory info about poll event with id=$eventId") + null + } } + + if (result.isFailure) { + Timber.w("failed to map event with id $eventId") + } + return result.getOrNull() } private fun convertToPollSummary( From cd1f41594dc760548a6b9521aa8b2be3d2f63da5 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Mon, 23 Jan 2023 14:43:21 +0100 Subject: [PATCH 22/64] Sync polls until now when landing on screen --- .../sdk/internal/session/room/RoomModule.kt | 5 + .../room/poll/DefaultPollHistoryService.kt | 36 ++++-- .../session/room/poll/SyncPollsTask.kt | 121 ++++++++++++++++++ 3 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/SyncPollsTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 56925d55d5..673a979633 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -93,8 +93,10 @@ import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask import org.matrix.android.sdk.internal.session.room.poll.DefaultGetLoadedPollsStatusTask import org.matrix.android.sdk.internal.session.room.poll.DefaultLoadMorePollsTask +import org.matrix.android.sdk.internal.session.room.poll.DefaultSyncPollsTask import org.matrix.android.sdk.internal.session.room.poll.GetLoadedPollsStatusTask import org.matrix.android.sdk.internal.session.room.poll.LoadMorePollsTask +import org.matrix.android.sdk.internal.session.room.poll.SyncPollsTask import org.matrix.android.sdk.internal.session.room.read.DefaultMarkAllRoomsReadTask import org.matrix.android.sdk.internal.session.room.read.DefaultSetReadMarkersTask import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask @@ -374,4 +376,7 @@ internal abstract class RoomModule { @Binds abstract fun bindFilterAndStoreEventsTask(task: DefaultFilterAndStoreEventsTask): FilterAndStoreEventsTask + + @Binds + abstract fun bindSyncPollsTask(task: DefaultSyncPollsTask): SyncPollsTask } 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 2fb63ef90e..52ea76b168 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 @@ -23,7 +23,8 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.realm.kotlin.where -import kotlinx.coroutines.delay +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus import org.matrix.android.sdk.api.session.room.poll.PollHistoryService @@ -50,6 +51,7 @@ internal class DefaultPollHistoryService @AssistedInject constructor( private val clock: Clock, private val loadMorePollsTask: LoadMorePollsTask, private val getLoadedPollsStatusTask: GetLoadedPollsStatusTask, + private val syncPollsTask: SyncPollsTask, private val timelineEventMapper: TimelineEventMapper, ) : PollHistoryService { @@ -71,20 +73,23 @@ internal class DefaultPollHistoryService @AssistedInject constructor( ) timelineService.createTimeline(eventId = null, settings = settings).also { it.start() } } + private val timelineMutex = Mutex() override fun dispose() { timeline.dispose() } override suspend fun loadMore(): LoadedPollsStatus { - val params = LoadMorePollsTask.Params( - timeline = timeline, - roomId = roomId, - currentTimestampMs = clock.epochMillis(), - loadingPeriodInDays = loadingPeriodInDays, - eventsPageSize = EVENTS_PAGE_SIZE, - ) - return loadMorePollsTask.execute(params) + return timelineMutex.withLock { + val params = LoadMorePollsTask.Params( + timeline = timeline, + roomId = roomId, + currentTimestampMs = clock.epochMillis(), + loadingPeriodInDays = loadingPeriodInDays, + eventsPageSize = EVENTS_PAGE_SIZE, + ) + loadMorePollsTask.execute(params) + } } override suspend fun getLoadedPollsStatus(): LoadedPollsStatus { @@ -96,10 +101,15 @@ internal class DefaultPollHistoryService @AssistedInject constructor( } override suspend fun syncPolls() { - // TODO unmock - // TODO when sync forward, jump to most recent event Id + paginate forward + jump to oldest eventId after - // TODO avoid possibility to call sync and loadMore at the same time from the service API, how? - delay(1000) + timelineMutex.withLock { + val params = SyncPollsTask.Params( + timeline = timeline, + roomId = roomId, + currentTimestampMs = clock.epochMillis(), + eventsPageSize = EVENTS_PAGE_SIZE, + ) + syncPollsTask.execute(params) + } } override fun getPollEvents(): LiveData> { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/SyncPollsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/SyncPollsTask.kt new file mode 100644 index 0000000000..e968095408 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/SyncPollsTask.kt @@ -0,0 +1,121 @@ +/* + * 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 + +import com.zhuinden.monarchy.Monarchy +import kotlinx.coroutines.delay +import org.matrix.android.sdk.api.session.events.model.isPoll +import org.matrix.android.sdk.api.session.events.model.isPollResponse +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +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.event.FilterAndStoreEventsTask +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 + +internal interface SyncPollsTask : Task { + data class Params( + val timeline: Timeline, + val roomId: String, + val currentTimestampMs: Long, + val eventsPageSize: Int, + ) +} + +internal class DefaultSyncPollsTask @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, +) : SyncPollsTask { + + override suspend fun execute(params: SyncPollsTask.Params) { + val currentPollHistoryStatus = getCurrentPollHistoryStatus(params.roomId) + + params.timeline.restartWithEventId(currentPollHistoryStatus.mostRecentEventIdReached) + + var loadStatus = LoadStatus(shouldLoadMore = true) + while (loadStatus.shouldLoadMore){ + loadStatus = fetchMorePollEventsForward(params) + } + + params.timeline.restartWithEventId(currentPollHistoryStatus.oldestEventIdReached) + } + + private suspend fun getCurrentPollHistoryStatus(roomId: String): PollHistoryStatusEntity { + return monarchy.awaitTransaction { realm -> + PollHistoryStatusEntity + .getOrCreate(realm, roomId) + .copy() + } + } + + private suspend fun fetchMorePollEventsForward(params: SyncPollsTask.Params): LoadStatus { + val events = params.timeline.awaitPaginate( + direction = Timeline.Direction.FORWARDS, + count = params.eventsPageSize, + ) + + val paginationState = params.timeline.getPaginationState(direction = Timeline.Direction.FORWARDS) + + return updatePollHistoryStatus( + roomId = params.roomId, + currentTimestampMs = params.currentTimestampMs, + events = events, + paginationState = paginationState, + ) + } + + private suspend fun updatePollHistoryStatus( + roomId: String, + currentTimestampMs: Long, + events: List, + paginationState: Timeline.PaginationState, + ): LoadStatus { + return monarchy.awaitTransaction { realm -> + val status = PollHistoryStatusEntity.getOrCreate(realm, roomId) + val mostRecentEventIdReached = status.mostRecentEventIdReached + + val mostRecentEvent = events + .maxByOrNull { it.root.originServerTs ?: Long.MIN_VALUE } + ?.root + + if (mostRecentEventIdReached == null) { + // save it for next forward pagination + status.mostRecentEventIdReached = mostRecentEvent?.eventId + } + + val mostRecentTimestamp = mostRecentEvent?.ageLocalTs + + val shouldLoadMore = paginationState.hasMoreToLoad && + (mostRecentTimestamp == null || mostRecentTimestamp < currentTimestampMs) + + LoadStatus(shouldLoadMore = shouldLoadMore) + } + } + + private class LoadStatus( + val shouldLoadMore: Boolean, + ) +} From 63026a3da5a3db8858fc412be5395605c0dfe863 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Mon, 23 Jan 2023 15:07:18 +0100 Subject: [PATCH 23/64] Using copy() on realm object when getting current poll history status --- .../internal/session/room/poll/GetLoadedPollsStatusTask.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.kt index 98b1e5931a..f273d2248a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.kt @@ -38,7 +38,9 @@ internal class DefaultGetLoadedPollsStatusTask @Inject constructor( override suspend fun execute(params: GetLoadedPollsStatusTask.Params): LoadedPollsStatus { return monarchy.awaitTransaction { realm -> - val status = PollHistoryStatusEntity.getOrCreate(realm, params.roomId) + val status = PollHistoryStatusEntity + .getOrCreate(realm, params.roomId) + .copy() LoadedPollsStatus( canLoadMore = status.isEndOfPollsBackward.not(), nbSyncedDays = status.getNbSyncedDays(params.currentTimestampMs), From 2f060952737dddc07dd7ec0b52cd1ac03a99a775 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Mon, 23 Jan 2023 15:16:09 +0100 Subject: [PATCH 24/64] Remove TODO --- .../sdk/internal/session/room/poll/DefaultPollHistoryService.kt | 1 - 1 file changed, 1 deletion(-) 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 52ea76b168..3ef0e7918a 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 @@ -64,7 +64,6 @@ internal class DefaultPollHistoryService @AssistedInject constructor( get() = LOADING_PERIOD_IN_DAYS private val timeline by lazy { - // TODO check if we need to add a way to avoid using the current filter in rooms val settings = TimelineSettings( initialSize = EVENTS_PAGE_SIZE, buildReadReceipts = false, From 2c2349aa639619d67477b7b649a8b77336d1ae4e Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Mon, 23 Jan 2023 15:35:33 +0100 Subject: [PATCH 25/64] Remove callback when RoomProfileFragment is destroyed --- .../im/vector/app/features/roomprofile/RoomProfileFragment.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt index 51885dbf39..91f57d33e9 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt @@ -207,6 +207,7 @@ class RoomProfileFragment : } override fun onDestroyView() { + roomProfileController.callback = null views.matrixProfileAppBarLayout.removeOnOffsetChangedListener(appBarStateChangeListener) views.matrixProfileRecyclerView.cleanup() appBarStateChangeListener = null From db2e2916a5b671c2f39c579787028ca83d9d4c6a Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Mon, 23 Jan 2023 15:37:18 +0100 Subject: [PATCH 26/64] Remove some TODOs --- .../sdk/internal/session/room/poll/LoadMorePollsTask.kt | 5 ----- 1 file changed, 5 deletions(-) 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 f22447f19c..939769b525 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 @@ -50,11 +50,6 @@ internal class DefaultLoadMorePollsTask @Inject constructor( while (shouldFetchMoreEventsBackward(currentPollHistoryStatus)) { currentPollHistoryStatus = fetchMorePollEventsBackward(params) } - // TODO - // 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 - // test on large room return LoadedPollsStatus( canLoadMore = currentPollHistoryStatus.isEndOfPollsBackward.not(), From 470218ca528938fe97ddc11152b2c19cfeadb79e Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Tue, 24 Jan 2023 15:37:53 +0100 Subject: [PATCH 27/64] Updating existing unit tests --- .../room/event/FilterAndStoreEventsTask.kt | 1 + .../polls/list/data/RoomPollRepository.kt | 1 + .../list/domain/DisposePollHistoryUseCase.kt | 1 + .../factory/PollItemViewStateFactoryTest.kt | 266 +++++++++++------- .../polls/RoomPollsViewModelTest.kt | 70 +++-- .../polls/list/data/RoomPollRepositoryTest.kt | 15 +- .../domain/GetLoadedPollsStatusUseCaseTest.kt | 12 +- .../polls/list/domain/GetPollsUseCaseTest.kt | 19 +- .../list/domain/LoadMorePollsUseCaseTest.kt | 1 + .../polls/list/domain/SyncPollsUseCaseTest.kt | 57 +++- 10 files changed, 300 insertions(+), 143 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt index e6e169b9b4..eb7364abd3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt @@ -58,6 +58,7 @@ internal class DefaultFilterAndStoreEventsTask @Inject constructor( private suspend fun addMissingEventsInDB(roomId: String, events: List) { monarchy.awaitTransaction { realm -> + // TODO we should insert TimelineEventEntity as well, how to do that???? val eventIdsToCheck = events.mapNotNull { it.eventId }.filter { it.isNotEmpty() } if (eventIdsToCheck.isNotEmpty()) { val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId } 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 d993302fb7..d6dfe12435 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 @@ -25,6 +25,7 @@ class RoomPollRepository @Inject constructor( private val roomPollDataSource: RoomPollDataSource, ) { + // TODO add unit tests fun dispose(roomId: String) { roomPollDataSource.dispose(roomId) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCase.kt index 21814b5a2b..4ed4e71501 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCase.kt @@ -26,6 +26,7 @@ class DisposePollHistoryUseCase @Inject constructor( private val roomPollRepository: RoomPollRepository, ) { + // TODO add unit tests fun execute(roomId: String) { roomPollRepository.dispose(roomId) } diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt index 8ee55d5b6e..067cb264e1 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt @@ -25,6 +25,9 @@ import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryDat import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.poll.PollViewState import im.vector.app.test.fakes.FakeStringProvider +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.model.message.MessagePollContent @@ -56,88 +59,69 @@ private val A_POLL_CONTENT = MessagePollContent( unstablePollCreationInfo = PollCreationInfo( question = PollQuestion( unstableQuestion = "What is your favourite coffee?" + ), kind = PollType.UNDISCLOSED_UNSTABLE, maxSelections = 1, answers = listOf( + PollAnswer( + id = A_POLL_OPTION_IDS[0], unstableAnswer = "Double Espresso" ), - kind = PollType.UNDISCLOSED_UNSTABLE, - maxSelections = 1, - answers = listOf( - PollAnswer( - id = A_POLL_OPTION_IDS[0], - unstableAnswer = "Double Espresso" - ), - PollAnswer( - id = A_POLL_OPTION_IDS[1], - unstableAnswer = "Macchiato" - ), - PollAnswer( - id = A_POLL_OPTION_IDS[2], - unstableAnswer = "Iced Coffee" - ), - ) + PollAnswer( + id = A_POLL_OPTION_IDS[1], unstableAnswer = "Macchiato" + ), + PollAnswer( + id = A_POLL_OPTION_IDS[2], unstableAnswer = "Iced Coffee" + ), + ) ) ) class PollItemViewStateFactoryTest { + private val fakeStringProvider = FakeStringProvider() + private val fakePollOptionViewStateFactory = mockk() + + private val pollItemViewStateFactory = PollItemViewStateFactory( + stringProvider = fakeStringProvider.instance, + pollOptionViewStateFactory = fakePollOptionViewStateFactory, + ) + @Test fun `given a sending poll state then poll is not votable and option states are PollSending`() { - val stringProvider = FakeStringProvider() - val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) - + // Given val sendingPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(sendState = SendState.SENDING) + val optionViewStates = listOf(PollOptionViewState.PollSending(optionId = "", optionAnswer = "")) + every { fakePollOptionViewStateFactory.createPollSendingOptions(A_POLL_CONTENT.getBestPollCreationInfo()) } returns optionViewStates + + // When val pollViewState = pollItemViewStateFactory.create( pollContent = A_POLL_CONTENT, informationData = sendingPollInformationData, ) + // Then pollViewState shouldBeEqualTo PollViewState( question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "", - votesStatus = stringProvider.instance.getString(R.string.poll_no_votes_cast), + votesStatus = fakeStringProvider.instance.getString(R.string.poll_no_votes_cast), canVote = false, - optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.map { answer -> - PollOptionViewState.PollSending( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "" - ) - }, + optionViewStates = optionViewStates, ) + verify { fakePollOptionViewStateFactory.createPollSendingOptions(A_POLL_CONTENT.getBestPollCreationInfo()) } } @Test fun `given a sent poll state when poll is closed then poll is not votable and option states are Ended`() { - val stringProvider = FakeStringProvider() - val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) - + // Given val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true) val closedPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = closedPollSummary) - - val pollViewState = pollItemViewStateFactory.create( - pollContent = A_POLL_CONTENT, - informationData = closedPollInformationData, + val optionViewStates = listOf( + PollOptionViewState.PollEnded( + optionId = "", optionAnswer = "", voteCount = 0, votePercentage = 0.0, isWinner = false + ) ) - - pollViewState shouldBeEqualTo PollViewState( - question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "", - votesStatus = stringProvider.instance.getQuantityString(R.plurals.poll_total_vote_count_after_ended, 0, 0), - canVote = false, - optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.map { answer -> - PollOptionViewState.PollEnded( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "", - voteCount = 0, - votePercentage = 0.0, - isWinner = false - ) - }, - ) - } - - @Test - fun `given a sent poll state with some decryption error when poll is closed then warning message is displayed`() { - // Given - val stringProvider = FakeStringProvider() - val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) - val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true, hasEncryptedRelatedEvents = true) - val closedPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = closedPollSummary) + every { + fakePollOptionViewStateFactory.createPollEndedOptions( + A_POLL_CONTENT.getBestPollCreationInfo(), + closedPollInformationData.pollResponseAggregatedSummary, + ) + } returns optionViewStates // When val pollViewState = pollItemViewStateFactory.create( @@ -146,42 +130,90 @@ class PollItemViewStateFactoryTest { ) // Then - pollViewState.votesStatus shouldBeEqualTo stringProvider.instance.getString(R.string.unable_to_decrypt_some_events_in_poll) + pollViewState shouldBeEqualTo PollViewState( + question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "", + votesStatus = fakeStringProvider.instance.getQuantityString(R.plurals.poll_total_vote_count_after_ended, 0, 0), + canVote = false, + optionViewStates = optionViewStates, + ) + verify { + fakePollOptionViewStateFactory.createPollEndedOptions( + A_POLL_CONTENT.getBestPollCreationInfo(), + closedPollInformationData.pollResponseAggregatedSummary, + ) + } + } + + @Test + fun `given a sent poll state with some decryption error when poll is closed then warning message is displayed`() { + // Given + val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true, hasEncryptedRelatedEvents = true) + val closedPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = closedPollSummary) + val optionViewStates = listOf( + PollOptionViewState.PollEnded( + optionId = "", optionAnswer = "", voteCount = 0, votePercentage = 0.0, isWinner = false + ) + ) + every { + fakePollOptionViewStateFactory.createPollEndedOptions( + A_POLL_CONTENT.getBestPollCreationInfo(), + closedPollInformationData.pollResponseAggregatedSummary, + ) + } returns optionViewStates + + // When + val pollViewState = pollItemViewStateFactory.create( + pollContent = A_POLL_CONTENT, + informationData = closedPollInformationData, + ) + + // Then + pollViewState.votesStatus shouldBeEqualTo fakeStringProvider.instance.getString(R.string.unable_to_decrypt_some_events_in_poll) } @Test fun `given a sent poll when undisclosed poll type is selected then poll is votable and option states are PollUndisclosed`() { - val stringProvider = FakeStringProvider() - val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) + // Given + val optionViewStates = listOf( + PollOptionViewState.PollUndisclosed( + optionId = "", + optionAnswer = "", + isSelected = false, + ) + ) + every { + fakePollOptionViewStateFactory.createPollUndisclosedOptions( + A_POLL_CONTENT.getBestPollCreationInfo(), + A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary, + ) + } returns optionViewStates + // When val pollViewState = pollItemViewStateFactory.create( pollContent = A_POLL_CONTENT, informationData = A_MESSAGE_INFORMATION_DATA, ) + // Then pollViewState shouldBeEqualTo PollViewState( question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "", - votesStatus = stringProvider.instance.getString(R.string.poll_undisclosed_not_ended), + votesStatus = fakeStringProvider.instance.getString(R.string.poll_undisclosed_not_ended), canVote = true, - optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.map { answer -> - PollOptionViewState.PollUndisclosed( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "", - isSelected = false - ) - }, + optionViewStates = optionViewStates, ) + verify { + fakePollOptionViewStateFactory.createPollUndisclosedOptions( + A_POLL_CONTENT.getBestPollCreationInfo(), + A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary, + ) + } } @Test fun `given a sent poll when my vote exists then poll is still votable and options states are PollVoted`() { - val stringProvider = FakeStringProvider() - val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) - + // Given val votedPollData = A_POLL_RESPONSE_DATA.copy( - totalVotes = 1, - myVote = A_POLL_OPTION_IDS[0], - votes = mapOf(A_POLL_OPTION_IDS[0] to PollVoteSummaryData(total = 1, percentage = 1.0)) + totalVotes = 1, myVote = A_POLL_OPTION_IDS[0], votes = mapOf(A_POLL_OPTION_IDS[0] to PollVoteSummaryData(total = 1, percentage = 1.0)) ) val disclosedPollContent = A_POLL_CONTENT.copy( unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( @@ -189,33 +221,46 @@ class PollItemViewStateFactoryTest { ), ) val votedInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = votedPollData) + val optionViewStates = listOf( + PollOptionViewState.PollVoted( + optionId = "", + optionAnswer = "", + voteCount = 0, + votePercentage = 0.0, + isSelected = false, + ) + ) + every { + fakePollOptionViewStateFactory.createPollVotedOptions( + disclosedPollContent.getBestPollCreationInfo(), + votedInformationData.pollResponseAggregatedSummary, + ) + } returns optionViewStates + // When val pollViewState = pollItemViewStateFactory.create( pollContent = disclosedPollContent, informationData = votedInformationData, ) + // Then pollViewState shouldBeEqualTo PollViewState( question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "", - votesStatus = stringProvider.instance.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, 1, 1), + votesStatus = fakeStringProvider.instance.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, 1, 1), canVote = true, - optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.mapIndexed { index, answer -> - PollOptionViewState.PollVoted( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "", - voteCount = if (index == 0) 1 else 0, - votePercentage = if (index == 0) 1.0 else 0.0, - isSelected = index == 0 - ) - }, + optionViewStates = optionViewStates, ) + verify { + fakePollOptionViewStateFactory.createPollVotedOptions( + disclosedPollContent.getBestPollCreationInfo(), + votedInformationData.pollResponseAggregatedSummary, + ) + } } @Test fun `given a sent poll with decryption failure when my vote exists then a warning message is displayed`() { // Given - val stringProvider = FakeStringProvider() - val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) val votedPollData = A_POLL_RESPONSE_DATA.copy( totalVotes = 1, myVote = A_POLL_OPTION_IDS[0], @@ -228,6 +273,21 @@ class PollItemViewStateFactoryTest { ), ) val votedInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = votedPollData) + val optionViewStates = listOf( + PollOptionViewState.PollVoted( + optionId = "", + optionAnswer = "", + voteCount = 0, + votePercentage = 0.0, + isSelected = false, + ) + ) + every { + fakePollOptionViewStateFactory.createPollVotedOptions( + disclosedPollContent.getBestPollCreationInfo(), + votedInformationData.pollResponseAggregatedSummary, + ) + } returns optionViewStates // When val pollViewState = pollItemViewStateFactory.create( @@ -236,34 +296,46 @@ class PollItemViewStateFactoryTest { ) // Then - pollViewState.votesStatus shouldBeEqualTo stringProvider.instance.getString(R.string.unable_to_decrypt_some_events_in_poll) + pollViewState.votesStatus shouldBeEqualTo fakeStringProvider.instance.getString(R.string.unable_to_decrypt_some_events_in_poll) } @Test fun `given a sent poll when poll type is disclosed then poll is votable and option view states are PollReady`() { - val stringProvider = FakeStringProvider() - val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) - + // Given val disclosedPollContent = A_POLL_CONTENT.copy( unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( kind = PollType.DISCLOSED_UNSTABLE ) ) + val optionViewStates = listOf( + PollOptionViewState.PollReady( + optionId = "", + optionAnswer = "", + ) + ) + every { + fakePollOptionViewStateFactory.createPollReadyOptions( + disclosedPollContent.getBestPollCreationInfo(), + ) + } returns optionViewStates + + // When val pollViewState = pollItemViewStateFactory.create( pollContent = disclosedPollContent, informationData = A_MESSAGE_INFORMATION_DATA, ) + // Then pollViewState shouldBeEqualTo PollViewState( question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "", - votesStatus = stringProvider.instance.getString(R.string.poll_no_votes_cast), + votesStatus = fakeStringProvider.instance.getString(R.string.poll_no_votes_cast), canVote = true, - optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.map { answer -> - PollOptionViewState.PollReady( - optionId = answer.id ?: "", - optionAnswer = answer.getBestAnswer() ?: "" - ) - }, + optionViewStates = optionViewStates, ) + verify { + fakePollOptionViewStateFactory.createPollReadyOptions( + disclosedPollContent.getBestPollCreationInfo(), + ) + } } } diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt index adbf32006e..084df03e64 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt @@ -17,23 +17,26 @@ package im.vector.app.features.roomprofile.polls import com.airbnb.mvrx.test.MavericksTestRule -import im.vector.app.features.roomprofile.polls.list.domain.GetLoadedPollsStatusUseCase +import im.vector.app.features.roomprofile.polls.list.domain.DisposePollHistoryUseCase 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.PollSummary +import im.vector.app.features.roomprofile.polls.list.ui.PollSummaryMapper import im.vector.app.test.test import im.vector.app.test.testDispatcher import io.mockk.coEvery -import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.every +import io.mockk.justRun import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import org.junit.Rule import org.junit.Test import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent private const val A_ROOM_ID = "room-id" @@ -42,31 +45,35 @@ class RoomPollsViewModelTest { @get:Rule val mavericksTestRule = MavericksTestRule(testDispatcher = testDispatcher) + private val initialState = RoomPollsViewState(A_ROOM_ID) private val fakeGetPollsUseCase = mockk() - private val fakeGetLoadedPollsStatusUseCase = mockk() private val fakeLoadMorePollsUseCase = mockk() private val fakeSyncPollsUseCase = mockk() - private val initialState = RoomPollsViewState(A_ROOM_ID) + private val fakeDisposePollHistoryUseCase = mockk() + private val fakePollSummaryMapper = mockk() private fun createViewModel(): RoomPollsViewModel { return RoomPollsViewModel( initialState = initialState, getPollsUseCase = fakeGetPollsUseCase, - getLoadedPollsStatusUseCase = fakeGetLoadedPollsStatusUseCase, loadMorePollsUseCase = fakeLoadMorePollsUseCase, syncPollsUseCase = fakeSyncPollsUseCase, + disposePollHistoryUseCase = fakeDisposePollHistoryUseCase, + pollSummaryMapper = fakePollSummaryMapper, ) } @Test fun `given viewModel when created then polls list is observed, sync is launched and viewState is updated`() { // Given - val loadedPollsStatus = givenGetLoadedPollsStatusSuccess() - givenSyncPollsWithSuccess() - val polls = listOf(givenAPollSummary()) + val loadedPollsStatus = givenSyncPollsWithSuccess() + val aPollEvent = givenAPollEvent() + val aPollSummary = givenAPollSummary() + val polls = listOf(aPollEvent) every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns flowOf(polls) + every { fakePollSummaryMapper.map(aPollEvent) } returns aPollSummary val expectedViewState = initialState.copy( - polls = polls, + polls = listOf(aPollSummary), canLoadMore = loadedPollsStatus.canLoadMore, nbSyncedDays = loadedPollsStatus.nbSyncedDays, ) @@ -81,6 +88,7 @@ class RoomPollsViewModelTest { .finish() verify { fakeGetPollsUseCase.execute(A_ROOM_ID) + fakePollSummaryMapper.map(aPollEvent) } coVerify { fakeSyncPollsUseCase.execute(A_ROOM_ID) } } @@ -88,10 +96,8 @@ class RoomPollsViewModelTest { @Test fun `given viewModel and error during sync process when created then error is raised in view event`() { // Given - givenGetLoadedPollsStatusSuccess() givenSyncPollsWithError(Exception()) - val polls = listOf(givenAPollSummary()) - every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns flowOf(polls) + every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns emptyFlow() // When val viewModel = createViewModel() @@ -104,17 +110,30 @@ class RoomPollsViewModelTest { coVerify { fakeSyncPollsUseCase.execute(A_ROOM_ID) } } + @Test + fun `given viewModel when calling onCleared then poll history is disposed`() { + // Given + givenSyncPollsWithSuccess() + every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns emptyFlow() + justRun { fakeDisposePollHistoryUseCase.execute(A_ROOM_ID) } + val viewModel = createViewModel() + + // When + viewModel.onCleared() + + // Then + verify { fakeDisposePollHistoryUseCase.execute(A_ROOM_ID) } + } + @Test fun `given viewModel when handle load more action then viewState is updated`() { // Given - val loadedPollsStatus = givenGetLoadedPollsStatusSuccess() - givenSyncPollsWithSuccess() - val polls = listOf(givenAPollSummary()) - every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns flowOf(polls) + val loadedPollsStatus = givenSyncPollsWithSuccess() + every { fakeGetPollsUseCase.execute(A_ROOM_ID) } returns emptyFlow() val newLoadedPollsStatus = givenLoadMoreWithSuccess() val viewModel = createViewModel() val stateAfterInit = initialState.copy( - polls = polls, + polls = emptyList(), canLoadMore = loadedPollsStatus.canLoadMore, nbSyncedDays = loadedPollsStatus.nbSyncedDays, ) @@ -139,8 +158,14 @@ class RoomPollsViewModelTest { return mockk() } - private fun givenSyncPollsWithSuccess() { - coJustRun { fakeSyncPollsUseCase.execute(A_ROOM_ID) } + private fun givenAPollEvent(): TimelineEvent { + return mockk() + } + + private fun givenSyncPollsWithSuccess(): LoadedPollsStatus { + val loadedPollsStatus = givenALoadedPollsStatus() + coEvery { fakeSyncPollsUseCase.execute(A_ROOM_ID) } returns loadedPollsStatus + return loadedPollsStatus } private fun givenSyncPollsWithError(error: Exception) { @@ -153,15 +178,10 @@ class RoomPollsViewModelTest { return loadedPollsStatus } - private fun givenGetLoadedPollsStatusSuccess(): LoadedPollsStatus { - val loadedPollsStatus = givenALoadedPollsStatus() - coEvery { fakeGetLoadedPollsStatusUseCase.execute(A_ROOM_ID) } returns loadedPollsStatus - return loadedPollsStatus - } - private fun givenALoadedPollsStatus(canLoadMore: Boolean = true, nbSyncedDays: Int = 10) = LoadedPollsStatus( canLoadMore = canLoadMore, nbSyncedDays = nbSyncedDays, + hasCompletedASyncBackward = false, ) } diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt index 49d9623c04..3883f0dafd 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt @@ -16,7 +16,7 @@ package im.vector.app.features.roomprofile.polls.list.data -import im.vector.app.features.roomprofile.polls.list.ui.PollSummary +import io.mockk.coEvery import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.every @@ -27,6 +27,8 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent private const val A_ROOM_ID = "room-id" @@ -41,7 +43,7 @@ class RoomPollRepositoryTest { @Test fun `given data source when getting polls then correct method of data source is called`() = runTest { // Given - val expectedPolls = listOf() + val expectedPolls = listOf() every { fakeRoomPollDataSource.getPolls(A_ROOM_ID) } returns flowOf(expectedPolls) // When @@ -53,20 +55,21 @@ class RoomPollRepositoryTest { } @Test - fun `given data source when getting loaded polls status then correct method of data source is called`() { + fun `given data source when getting loaded polls status then correct method of data source is called`() = runTest { // Given val expectedStatus = LoadedPollsStatus( canLoadMore = true, - nbLoadedDays = 10, + nbSyncedDays = 10, + hasCompletedASyncBackward = false, ) - every { fakeRoomPollDataSource.getLoadedPollsStatus(A_ROOM_ID) } returns expectedStatus + coEvery { fakeRoomPollDataSource.getLoadedPollsStatus(A_ROOM_ID) } returns expectedStatus // When val result = roomPollRepository.getLoadedPollsStatus(A_ROOM_ID) // Then result shouldBeEqualTo expectedStatus - verify { fakeRoomPollDataSource.getLoadedPollsStatus(A_ROOM_ID) } + coVerify { fakeRoomPollDataSource.getLoadedPollsStatus(A_ROOM_ID) } } @Test diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt index 12c23797f0..eeaf7803a6 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt @@ -17,9 +17,10 @@ package im.vector.app.features.roomprofile.polls.list.domain import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository -import io.mockk.every +import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.mockk -import io.mockk.verify +import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus @@ -33,20 +34,21 @@ class GetLoadedPollsStatusUseCaseTest { ) @Test - fun `given repo when execute then correct method of repo is called`() { + fun `given repo when execute then correct method of repo is called`() = runTest { // Given val aRoomId = "roomId" val expectedStatus = LoadedPollsStatus( canLoadMore = true, nbSyncedDays = 10, + hasCompletedASyncBackward = true, ) - every { fakeRoomPollRepository.getLoadedPollsStatus(aRoomId) } returns expectedStatus + coEvery { fakeRoomPollRepository.getLoadedPollsStatus(aRoomId) } returns expectedStatus // When val status = getLoadedPollsStatusUseCase.execute(aRoomId) // Then status shouldBeEqualTo expectedStatus - verify { fakeRoomPollRepository.getLoadedPollsStatus(aRoomId) } + coVerify { fakeRoomPollRepository.getLoadedPollsStatus(aRoomId) } } } diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCaseTest.kt index e69b9287f8..f29a4844d7 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetPollsUseCaseTest.kt @@ -17,8 +17,6 @@ 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 im.vector.app.test.fixtures.RoomPollFixture import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -27,6 +25,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent class GetPollsUseCaseTest { private val fakeRoomPollRepository = mockk() @@ -39,16 +38,16 @@ class GetPollsUseCaseTest { fun `given repo when execute then correct method of repo is called and polls are sorted most recent first`() = runTest { // Given val aRoomId = "roomId" - val poll1 = RoomPollFixture.anActivePollSummary(timestamp = 1) - val poll2 = RoomPollFixture.anActivePollSummary(timestamp = 2) - val poll3 = RoomPollFixture.anActivePollSummary(timestamp = 3) - val polls = listOf( + val poll1 = givenTimelineEvent(timestamp = 1) + val poll2 = givenTimelineEvent(timestamp = 2) + val poll3 = givenTimelineEvent(timestamp = 3) + val polls = listOf( poll1, poll2, poll3, ) every { fakeRoomPollRepository.getPolls(aRoomId) } returns flowOf(polls) - val expectedPolls = listOf( + val expectedPolls = listOf( poll3, poll2, poll1, @@ -60,4 +59,10 @@ class GetPollsUseCaseTest { result shouldBeEqualTo expectedPolls verify { fakeRoomPollRepository.getPolls(aRoomId) } } + + private fun givenTimelineEvent(timestamp: Long): TimelineEvent { + return mockk().also { + every { it.root.originServerTs } returns timestamp + } + } } diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt index 4c769de222..d2f63c2a37 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt @@ -40,6 +40,7 @@ class LoadMorePollsUseCaseTest { val loadedPollsStatus = LoadedPollsStatus( canLoadMore = true, nbSyncedDays = 10, + hasCompletedASyncBackward = true, ) coEvery { fakeRoomPollRepository.loadMorePolls(aRoomId) } returns loadedPollsStatus diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCaseTest.kt index 040514e301..e60214dde7 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCaseTest.kt @@ -17,30 +17,81 @@ package im.vector.app.features.roomprofile.polls.list.domain import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository +import io.mockk.coEvery import io.mockk.coJustRun import io.mockk.coVerify +import io.mockk.coVerifyOrder 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 SyncPollsUseCaseTest { private val fakeRoomPollRepository = mockk() + private val fakeGetLoadedPollsStatusUseCase = mockk() + private val fakeLoadMorePollsUseCase = mockk() private val syncPollsUseCase = SyncPollsUseCase( roomPollRepository = fakeRoomPollRepository, + getLoadedPollsStatusUseCase = fakeGetLoadedPollsStatusUseCase, + loadMorePollsUseCase = fakeLoadMorePollsUseCase, ) @Test - fun `given repo when execute then correct method of repo is called`() = runTest { + fun `given it has completed a sync backward when execute then only sync process is called`() = runTest { // Given val aRoomId = "roomId" + val aLoadedStatus = LoadedPollsStatus( + canLoadMore = true, + nbSyncedDays = 10, + hasCompletedASyncBackward = true, + ) coJustRun { fakeRoomPollRepository.syncPolls(aRoomId) } + coEvery { fakeGetLoadedPollsStatusUseCase.execute(aRoomId) } returns aLoadedStatus // When - syncPollsUseCase.execute(aRoomId) + val result = syncPollsUseCase.execute(aRoomId) // Then - coVerify { fakeRoomPollRepository.syncPolls(aRoomId) } + result shouldBeEqualTo aLoadedStatus + coVerifyOrder { + fakeRoomPollRepository.syncPolls(aRoomId) + fakeGetLoadedPollsStatusUseCase.execute(aRoomId) + } + coVerify(inverse = true) { + fakeLoadMorePollsUseCase.execute(any()) + } + } + + @Test + fun `given it has not completed a sync backward when execute then sync process and load more is called`() = runTest { + // Given + val aRoomId = "roomId" + val aLoadedStatus = LoadedPollsStatus( + canLoadMore = true, + nbSyncedDays = 10, + hasCompletedASyncBackward = false, + ) + val anUpdatedLoadedStatus = LoadedPollsStatus( + canLoadMore = true, + nbSyncedDays = 10, + hasCompletedASyncBackward = true, + ) + coJustRun { fakeRoomPollRepository.syncPolls(aRoomId) } + coEvery { fakeGetLoadedPollsStatusUseCase.execute(aRoomId) } returns aLoadedStatus + coEvery { fakeLoadMorePollsUseCase.execute(aRoomId) } returns anUpdatedLoadedStatus + + // When + val result = syncPollsUseCase.execute(aRoomId) + + // Then + result shouldBeEqualTo anUpdatedLoadedStatus + coVerifyOrder { + fakeRoomPollRepository.syncPolls(aRoomId) + fakeGetLoadedPollsStatusUseCase.execute(aRoomId) + fakeLoadMorePollsUseCase.execute(aRoomId) + } } } From 184a25b811a3939682ef754bd4583858874e5022 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Tue, 24 Jan 2023 15:43:43 +0100 Subject: [PATCH 28/64] Adding unit tests for dispose methods --- .../polls/list/data/RoomPollRepository.kt | 1 - .../list/domain/DisposePollHistoryUseCase.kt | 4 -- .../polls/list/data/RoomPollRepositoryTest.kt | 13 ++++++ .../domain/DisposePollHistoryUseCaseTest.kt | 45 +++++++++++++++++++ 4 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCaseTest.kt 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 d6dfe12435..d993302fb7 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 @@ -25,7 +25,6 @@ class RoomPollRepository @Inject constructor( private val roomPollDataSource: RoomPollDataSource, ) { - // TODO add unit tests fun dispose(roomId: String) { roomPollDataSource.dispose(roomId) } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCase.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCase.kt index 4ed4e71501..f1cf031f73 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCase.kt @@ -17,16 +17,12 @@ package im.vector.app.features.roomprofile.polls.list.domain import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository -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 DisposePollHistoryUseCase @Inject constructor( private val roomPollRepository: RoomPollRepository, ) { - // TODO add unit tests fun execute(roomId: String) { roomPollRepository.dispose(roomId) } diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt index 3883f0dafd..e57b52a812 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt @@ -20,6 +20,7 @@ import io.mockk.coEvery import io.mockk.coJustRun import io.mockk.coVerify import io.mockk.every +import io.mockk.justRun import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.flow.firstOrNull @@ -40,6 +41,18 @@ class RoomPollRepositoryTest { roomPollDataSource = fakeRoomPollDataSource, ) + @Test + fun `given data source when dispose then correct method of data source is called`() { + // Given + justRun { fakeRoomPollDataSource.dispose(A_ROOM_ID) } + + // When + roomPollRepository.dispose(A_ROOM_ID) + + // Then + verify { fakeRoomPollDataSource.dispose(A_ROOM_ID) } + } + @Test fun `given data source when getting polls then correct method of data source is called`() = runTest { // Given diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCaseTest.kt new file mode 100644 index 0000000000..0063d9bfd5 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/DisposePollHistoryUseCaseTest.kt @@ -0,0 +1,45 @@ +/* + * 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.domain + +import im.vector.app.features.roomprofile.polls.list.data.RoomPollRepository +import io.mockk.coVerify +import io.mockk.justRun +import io.mockk.mockk +import org.junit.Test + +internal class DisposePollHistoryUseCaseTest { + + private val fakeRoomPollRepository = mockk() + + private val disposePollHistoryUseCase = DisposePollHistoryUseCase( + roomPollRepository = fakeRoomPollRepository, + ) + + @Test + fun `given repo when execute then correct method of repo is called`() { + // Given + val aRoomId = "roomId" + justRun { fakeRoomPollRepository.dispose(aRoomId) } + + // When + disposePollHistoryUseCase.execute(aRoomId) + + // Then + coVerify { fakeRoomPollRepository.dispose(aRoomId) } + } +} From 983649d89a7e2001e6830afc307472735ab636ce Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Tue, 24 Jan 2023 15:49:31 +0100 Subject: [PATCH 29/64] Adding sdk changelog entry --- changelog.d/7864.sdk | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7864.sdk diff --git a/changelog.d/7864.sdk b/changelog.d/7864.sdk new file mode 100644 index 0000000000..b7c6a5b339 --- /dev/null +++ b/changelog.d/7864.sdk @@ -0,0 +1 @@ +[Poll] Adding PollHistoryService From 21cee773e2dc35face9db14408952e241acc404b Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Tue, 24 Jan 2023 16:17:24 +0100 Subject: [PATCH 30/64] Adding unit tests for RoomPollDataSource --- .../polls/list/data/RoomPollDataSource.kt | 1 - .../polls/list/data/RoomPollDataSourceTest.kt | 130 ++++++++++++++++++ .../app/test/fakes/FakePollHistoryService.kt | 75 ++++++++++ .../java/im/vector/app/test/fakes/FakeRoom.kt | 3 + 4 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSourceTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakePollHistoryService.kt 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 60b53a90b4..3a65297fde 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 @@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.session.room.poll.PollHistoryService import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject -// TODO add unit tests class RoomPollDataSource @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, ) { diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSourceTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSourceTest.kt new file mode 100644 index 0000000000..11006b10e8 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSourceTest.kt @@ -0,0 +1,130 @@ +/* + * 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.data + +import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeFlowLiveDataConversions +import im.vector.app.test.fakes.FakePollHistoryService +import im.vector.app.test.fakes.givenAsFlow +import io.mockk.unmockkAll +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +private const val A_ROOM_ID = "room-id" + +internal class RoomPollDataSourceTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val roomPollDataSource = RoomPollDataSource( + activeSessionHolder = fakeActiveSessionHolder.instance, + ) + + @Test + fun `given poll history service when dispose then correct method of service is called`() { + // Given + val fakePollHistoryService = givenPollHistoryService() + fakePollHistoryService.givenDispose() + + // When + roomPollDataSource.dispose(A_ROOM_ID) + + // Then + fakePollHistoryService.verifyDispose() + } + + @Test + fun `given poll history service when get polls then correct method of service is called and correct result is returned`() = runTest { + // Given + val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions() + fakeFlowLiveDataConversions.setup() + val fakePollHistoryService = givenPollHistoryService() + val pollEvents = listOf() + fakePollHistoryService + .givenGetPollsReturns(pollEvents) + .givenAsFlow() + + // When + val result = roomPollDataSource.getPolls(A_ROOM_ID).firstOrNull() + + // Then + result shouldBeEqualTo pollEvents + fakePollHistoryService.verifyGetPolls() + unmockkAll() + } + + @Test + fun `given poll history service when get loaded polls then correct method of service is called and correct result is returned`() = runTest { + // Given + val fakePollHistoryService = givenPollHistoryService() + val aLoadedPollsStatus = givenALoadedPollsStatus() + fakePollHistoryService.givenGetLoadedPollsStatusReturns(aLoadedPollsStatus) + + // When + val result = roomPollDataSource.getLoadedPollsStatus(A_ROOM_ID) + + // Then + result shouldBeEqualTo aLoadedPollsStatus + fakePollHistoryService.verifyGetLoadedPollsStatus() + } + + @Test + fun `given poll history service when load more then correct method of service is called and correct result is returned`() = runTest { + // Given + val fakePollHistoryService = givenPollHistoryService() + val aLoadedPollsStatus = givenALoadedPollsStatus() + fakePollHistoryService.givenLoadMoreReturns(aLoadedPollsStatus) + + // When + val result = roomPollDataSource.loadMorePolls(A_ROOM_ID) + + // Then + result shouldBeEqualTo aLoadedPollsStatus + fakePollHistoryService.verifyLoadMore() + } + + @Test + fun `given poll history service when sync polls then correct method of service is called`() = runTest { + // Given + val fakePollHistoryService = givenPollHistoryService() + fakePollHistoryService.givenSyncPollsSuccess() + + // When + roomPollDataSource.syncPolls(A_ROOM_ID) + + // Then + fakePollHistoryService.verifySyncPolls() + } + + private fun givenPollHistoryService(): FakePollHistoryService { + return fakeActiveSessionHolder + .fakeSession + .fakeRoomService + .getRoom(A_ROOM_ID) + .pollHistoryService() + } + + private fun givenALoadedPollsStatus() = LoadedPollsStatus( + canLoadMore = true, + nbSyncedDays = 10, + hasCompletedASyncBackward = true, + ) +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakePollHistoryService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakePollHistoryService.kt new file mode 100644 index 0000000000..c934c3acde --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakePollHistoryService.kt @@ -0,0 +1,75 @@ +/* + * 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.test.fakes + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import io.mockk.coEvery +import io.mockk.coJustRun +import io.mockk.coVerify +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.verify +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 + +class FakePollHistoryService : PollHistoryService by mockk() { + + fun givenDispose() { + justRun { dispose() } + } + + fun verifyDispose() { + verify { dispose() } + } + + fun givenGetPollsReturns(events: List): LiveData> { + return MutableLiveData(events).also { + every { getPollEvents() } returns it + } + } + + fun verifyGetPolls() { + verify { getPollEvents() } + } + + fun givenGetLoadedPollsStatusReturns(status: LoadedPollsStatus) { + coEvery { getLoadedPollsStatus() } returns status + } + + fun verifyGetLoadedPollsStatus() { + coVerify { getLoadedPollsStatus() } + } + + fun givenLoadMoreReturns(status: LoadedPollsStatus) { + coEvery { loadMore() } returns status + } + + fun verifyLoadMore() { + coVerify { loadMore() } + } + + fun givenSyncPollsSuccess() { + coJustRun { syncPolls() } + } + + fun verifySyncPolls() { + coVerify { syncPolls() } + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt index 7835c314ef..d3703f11c4 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt @@ -25,6 +25,7 @@ class FakeRoom( private val fakeTimelineService: FakeTimelineService = FakeTimelineService(), private val fakeRelationService: FakeRelationService = FakeRelationService(), private val fakeStateService: FakeStateService = FakeStateService(), + private val fakePollHistoryService: FakePollHistoryService = FakePollHistoryService(), ) : Room by mockk() { override fun locationSharingService() = fakeLocationSharingService @@ -36,4 +37,6 @@ class FakeRoom( override fun relationService() = fakeRelationService override fun stateService() = fakeStateService + + override fun pollHistoryService() = fakePollHistoryService } From 326ece4b082216de07f28269e4dbf38821c9285f Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Tue, 24 Jan 2023 17:15:35 +0100 Subject: [PATCH 31/64] Fixing code styling issue --- .../sdk/internal/session/room/poll/SyncPollsTask.kt | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/SyncPollsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/SyncPollsTask.kt index e968095408..4a83f0f870 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/SyncPollsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/SyncPollsTask.kt @@ -17,22 +17,11 @@ package org.matrix.android.sdk.internal.session.room.poll import com.zhuinden.monarchy.Monarchy -import kotlinx.coroutines.delay -import org.matrix.android.sdk.api.session.events.model.isPoll -import org.matrix.android.sdk.api.session.events.model.isPollResponse -import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent 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.event.FilterAndStoreEventsTask -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 @@ -56,7 +45,7 @@ internal class DefaultSyncPollsTask @Inject constructor( params.timeline.restartWithEventId(currentPollHistoryStatus.mostRecentEventIdReached) var loadStatus = LoadStatus(shouldLoadMore = true) - while (loadStatus.shouldLoadMore){ + while (loadStatus.shouldLoadMore) { loadStatus = fetchMorePollEventsForward(params) } From cfc67d5b67d10a0409317a8474a5aec2755545fd Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Tue, 24 Jan 2023 17:58:52 +0100 Subject: [PATCH 32/64] Adding unit tests for PollSummaryMapper --- .../room/poll/DefaultPollHistoryService.kt | 1 - .../polls/list/ui/PollSummaryMapper.kt | 1 - .../polls/list/ui/PollSummaryMapperTest.kt | 201 ++++++++++++++++++ 3 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummaryMapperTest.kt 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 3ef0e7918a..51c8b83191 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 @@ -43,7 +43,6 @@ import timber.log.Timber private const val LOADING_PERIOD_IN_DAYS = 30 private const val EVENTS_PAGE_SIZE = 250 -// TODO add unit tests internal class DefaultPollHistoryService @AssistedInject constructor( @Assisted private val roomId: String, @Assisted private val timelineService: TimelineService, 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 index 9d4128e0ac..64c712e61f 100644 --- 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 @@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import timber.log.Timber import javax.inject.Inject -// TODO add unit tests class PollSummaryMapper @Inject constructor( private val pollResponseDataFactory: PollResponseDataFactory, private val pollOptionViewStateFactory: PollOptionViewStateFactory, diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummaryMapperTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummaryMapperTest.kt new file mode 100644 index 0000000000..b523365970 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/ui/PollSummaryMapperTest.kt @@ -0,0 +1,201 @@ +/* + * 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.factory.PollOptionViewStateFactory +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 io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo +import org.matrix.android.sdk.api.session.room.model.message.PollQuestion +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +private const val AN_EVENT_ID = "event-id" +private const val AN_EVENT_TIMESTAMP = 123L +private const val A_POLL_TITLE = "poll-title" + +internal class PollSummaryMapperTest { + + private val fakePollResponseDataFactory = mockk() + private val fakePollOptionViewStateFactory = mockk() + + private val pollSummaryMapper = PollSummaryMapper( + pollResponseDataFactory = fakePollResponseDataFactory, + pollOptionViewStateFactory = fakePollOptionViewStateFactory, + ) + + @Before + fun setup() { + mockkStatic("im.vector.app.core.extensions.TimelineEventKt") + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given a not ended poll event when mapping to model then result is active poll`() { + // Given + val pollStartedEvent = givenAPollTimelineEvent( + eventId = AN_EVENT_ID, + creationTimestamp = AN_EVENT_TIMESTAMP, + pollTitle = A_POLL_TITLE, + isClosed = false, + ) + val expectedResult = PollSummary.ActivePoll( + id = AN_EVENT_ID, + creationTimestamp = AN_EVENT_TIMESTAMP, + title = A_POLL_TITLE, + ) + + // When + val result = pollSummaryMapper.map(pollStartedEvent) + + // Then + result shouldBeEqualTo expectedResult + } + + @Test + fun `given an ended poll event when mapping to model then result is ended poll`() { + // Given + val totalVotes = 10 + val winnerOptions = listOf() + val endedPollEvent = givenAPollTimelineEvent( + eventId = AN_EVENT_ID, + creationTimestamp = AN_EVENT_TIMESTAMP, + pollTitle = A_POLL_TITLE, + isClosed = true, + totalVotes = totalVotes, + winnerOptions = winnerOptions, + ) + val expectedResult = PollSummary.EndedPoll( + id = AN_EVENT_ID, + creationTimestamp = AN_EVENT_TIMESTAMP, + title = A_POLL_TITLE, + totalVotes = totalVotes, + winnerOptions = winnerOptions, + ) + + // When + val result = pollSummaryMapper.map(endedPollEvent) + + // Then + result shouldBeEqualTo expectedResult + } + + @Test + fun `given missing data in event when mapping to model then result is null`() { + // Given + val noIdPollEvent = givenAPollTimelineEvent( + eventId = "", + creationTimestamp = AN_EVENT_TIMESTAMP, + pollTitle = A_POLL_TITLE, + isClosed = false, + ) + val noTimestampPollEvent = givenAPollTimelineEvent( + eventId = AN_EVENT_ID, + creationTimestamp = 0, + pollTitle = A_POLL_TITLE, + isClosed = false, + ) + val notAPollEvent = givenATimelineEvent( + eventId = AN_EVENT_ID, + creationTimestamp = 0, + content = mockk() + ) + + // When + val result1 = pollSummaryMapper.map(noIdPollEvent) + val result2 = pollSummaryMapper.map(noTimestampPollEvent) + val result3 = pollSummaryMapper.map(notAPollEvent) + + // Then + result1 shouldBe null + result2 shouldBe null + result3 shouldBe null + } + + private fun givenATimelineEvent( + eventId: String, + creationTimestamp: Long, + content: MessageContent, + ): TimelineEvent { + val timelineEvent = mockk() + every { timelineEvent.root.eventId } returns eventId + every { timelineEvent.root.originServerTs } returns creationTimestamp + every { timelineEvent.getVectorLastMessageContent() } returns content + return timelineEvent + } + + private fun givenAPollTimelineEvent( + eventId: String, + creationTimestamp: Long, + pollTitle: String, + isClosed: Boolean, + totalVotes: Int = 0, + winnerOptions: List = emptyList(), + ): TimelineEvent { + val pollCreationInfo = givenPollCreationInfo(pollTitle) + val messageContent = givenAMessagePollContent(pollCreationInfo) + val timelineEvent = givenATimelineEvent(eventId, creationTimestamp, messageContent) + val pollResponseData = givenAPollResponseData(isClosed, totalVotes) + every { fakePollResponseDataFactory.create(timelineEvent) } returns pollResponseData + every { + fakePollOptionViewStateFactory.createPollEndedOptions( + pollCreationInfo, + pollResponseData + ) + } returns winnerOptions + + return timelineEvent + } + + private fun givenAMessagePollContent(pollCreationInfo: PollCreationInfo): MessagePollContent { + return MessagePollContent( + unstablePollCreationInfo = pollCreationInfo, + ) + } + + private fun givenPollCreationInfo(pollTitle: String): PollCreationInfo { + return PollCreationInfo( + question = PollQuestion(unstableQuestion = pollTitle), + ) + } + + private fun givenAPollResponseData(isClosed: Boolean, totalVotes: Int): PollResponseData { + return PollResponseData( + myVote = "", + votes = emptyMap(), + isClosed = isClosed, + totalVotes = totalVotes, + ) + } +} From 41bb743cf4c608454dbe5643de7d2716400733a4 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Wed, 25 Jan 2023 10:54:37 +0100 Subject: [PATCH 33/64] Adding unit tests for PollOptionViewStateFactory --- .../factory/PollOptionViewStateFactory.kt | 2 - .../factory/PollItemViewStateFactoryTest.kt | 40 +---- .../factory/PollOptionViewStateFactoryTest.kt | 157 ++++++++++++++++++ .../vector/app/test/fixtures/PollFixture.kt | 67 ++++++++ .../app/test/fixtures/RoomPollFixture.kt | 47 ------ 5 files changed, 228 insertions(+), 85 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollOptionViewStateFactoryTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fixtures/PollFixture.kt delete mode 100644 vector/src/test/java/im/vector/app/test/fixtures/RoomPollFixture.kt diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollOptionViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollOptionViewStateFactory.kt index 3000164f74..875675745c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollOptionViewStateFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollOptionViewStateFactory.kt @@ -21,12 +21,10 @@ import im.vector.app.features.home.room.detail.timeline.item.PollResponseData import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo import javax.inject.Inject -// TODO add unit tests class PollOptionViewStateFactory @Inject constructor() { fun createPollEndedOptions(pollCreationInfo: PollCreationInfo?, pollResponseData: PollResponseData?): List { val winnerVoteCount = pollResponseData?.winnerVoteCount - return pollCreationInfo?.answers?.map { answer -> val voteSummary = pollResponseData?.getVoteSummaryOfAnOption(answer.id ?: "") PollOptionViewState.PollEnded( diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt index 067cb264e1..99c6c69849 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt @@ -25,6 +25,10 @@ import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryDat import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.poll.PollViewState import im.vector.app.test.fakes.FakeStringProvider +import im.vector.app.test.fixtures.PollFixture.A_MESSAGE_INFORMATION_DATA +import im.vector.app.test.fixtures.PollFixture.A_POLL_CONTENT +import im.vector.app.test.fixtures.PollFixture.A_POLL_OPTION_IDS +import im.vector.app.test.fixtures.PollFixture.A_POLL_RESPONSE_DATA import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -37,42 +41,6 @@ import org.matrix.android.sdk.api.session.room.model.message.PollQuestion import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.send.SendState -private val A_MESSAGE_INFORMATION_DATA = MessageInformationData( - eventId = "eventId", - senderId = "senderId", - ageLocalTS = 0, - avatarUrl = "", - sendState = SendState.SENT, - messageLayout = TimelineMessageLayout.Default(showAvatar = true, showDisplayName = true, showTimestamp = true), - reactionsSummary = ReactionsSummaryData(), - sentByMe = true, -) - -private val A_POLL_RESPONSE_DATA = PollResponseData( - myVote = null, - votes = emptyMap(), -) - -private val A_POLL_OPTION_IDS = listOf("5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", "ec1a4db0-46d8-4d7a-9bb6-d80724715938", "3677ca8e-061b-40ab-bffe-b22e4e88fcad") - -private val A_POLL_CONTENT = MessagePollContent( - unstablePollCreationInfo = PollCreationInfo( - question = PollQuestion( - unstableQuestion = "What is your favourite coffee?" - ), kind = PollType.UNDISCLOSED_UNSTABLE, maxSelections = 1, answers = listOf( - PollAnswer( - id = A_POLL_OPTION_IDS[0], unstableAnswer = "Double Espresso" - ), - PollAnswer( - id = A_POLL_OPTION_IDS[1], unstableAnswer = "Macchiato" - ), - PollAnswer( - id = A_POLL_OPTION_IDS[2], unstableAnswer = "Iced Coffee" - ), - ) - ) -) - class PollItemViewStateFactoryTest { private val fakeStringProvider = FakeStringProvider() diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollOptionViewStateFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollOptionViewStateFactoryTest.kt new file mode 100644 index 0000000000..285cff7d63 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollOptionViewStateFactoryTest.kt @@ -0,0 +1,157 @@ +/* + * 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.home.room.detail.timeline.factory + +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState +import im.vector.app.features.home.room.detail.timeline.item.PollVoteSummaryData +import im.vector.app.test.fixtures.PollFixture.A_POLL_CONTENT +import im.vector.app.test.fixtures.PollFixture.A_POLL_OPTION_IDS +import im.vector.app.test.fixtures.PollFixture.A_POLL_RESPONSE_DATA +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.session.room.model.message.PollType + +internal class PollOptionViewStateFactoryTest { + + private val pollOptionViewStateFactory = PollOptionViewStateFactory() + + @Test + fun `given poll data when creating ended poll options then correct options are returned`() { + // Given + val winnerVotesCount = 0 + val pollResponseData = A_POLL_RESPONSE_DATA.copy( + isClosed = true, + winnerVoteCount = winnerVotesCount, + ) + val pollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo() + val expectedOptions = pollCreationInfo?.answers?.map { answer -> + PollOptionViewState.PollEnded( + optionId = answer.id.orEmpty(), + optionAnswer = answer.getBestAnswer().orEmpty(), + voteCount = 0, + votePercentage = 0.0, + isWinner = false, + ) + } + + // When + val result = pollOptionViewStateFactory.createPollEndedOptions( + pollCreationInfo = pollCreationInfo, + pollResponseData = pollResponseData, + ) + + // Then + result shouldBeEqualTo expectedOptions + } + + @Test + fun `given poll data when creating sending poll options then correct options are returned`() { + // Given + val pollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo() + val expectedOptions = pollCreationInfo?.answers?.map { answer -> + PollOptionViewState.PollSending( + optionId = answer.id.orEmpty(), + optionAnswer = answer.getBestAnswer().orEmpty(), + ) + } + + // When + val result = pollOptionViewStateFactory.createPollSendingOptions( + pollCreationInfo = pollCreationInfo, + ) + + // Then + result shouldBeEqualTo expectedOptions + } + + @Test + fun `given poll data when creating undisclosed poll options then correct options are returned`() { + // Given + val pollResponseData = A_POLL_RESPONSE_DATA + val pollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo() + val expectedOptions = pollCreationInfo?.answers?.map { answer -> + PollOptionViewState.PollUndisclosed( + optionId = answer.id.orEmpty(), + optionAnswer = answer.getBestAnswer().orEmpty(), + isSelected = false, + ) + } + + // When + val result = pollOptionViewStateFactory.createPollUndisclosedOptions( + pollCreationInfo = pollCreationInfo, + pollResponseData = pollResponseData, + ) + + // Then + result shouldBeEqualTo expectedOptions + } + + @Test + fun `given poll data when creating voted poll options then correct options are returned`() { + // Given + val pollResponseData = A_POLL_RESPONSE_DATA.copy( + totalVotes = 1, + myVote = A_POLL_OPTION_IDS[0], + votes = mapOf(A_POLL_OPTION_IDS[0] to PollVoteSummaryData(total = 1, percentage = 1.0)), + ) + val disclosedPollContent = A_POLL_CONTENT.copy( + unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( + kind = PollType.DISCLOSED_UNSTABLE, + ), + ) + val pollCreationInfo = disclosedPollContent.getBestPollCreationInfo() + val expectedOptions = pollCreationInfo?.answers?.mapIndexed { index, answer -> + PollOptionViewState.PollVoted( + optionId = answer.id.orEmpty(), + optionAnswer = answer.getBestAnswer().orEmpty(), + voteCount = if (index == 0) 1 else 0, + votePercentage = if (index == 0) 1.0 else 0.0, + isSelected = index == 0, + ) + } + + // When + val result = pollOptionViewStateFactory.createPollVotedOptions( + pollCreationInfo = pollCreationInfo, + pollResponseData = pollResponseData, + ) + + // Then + result shouldBeEqualTo expectedOptions + } + + @Test + fun `given poll data when creating ready poll options then correct options are returned`() { + // Given + val pollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo() + val expectedOptions = pollCreationInfo?.answers?.map { answer -> + PollOptionViewState.PollReady( + optionId = answer.id.orEmpty(), + optionAnswer = answer.getBestAnswer().orEmpty(), + ) + } + + // When + val result = pollOptionViewStateFactory.createPollReadyOptions( + pollCreationInfo = pollCreationInfo, + ) + + // Then + result shouldBeEqualTo expectedOptions + } +} diff --git a/vector/src/test/java/im/vector/app/test/fixtures/PollFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/PollFixture.kt new file mode 100644 index 0000000000..24e037b299 --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fixtures/PollFixture.kt @@ -0,0 +1,67 @@ +/* + * 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.test.fixtures + +import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.app.features.home.room.detail.timeline.item.PollResponseData +import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryData +import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.model.message.PollAnswer +import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo +import org.matrix.android.sdk.api.session.room.model.message.PollQuestion +import org.matrix.android.sdk.api.session.room.model.message.PollType +import org.matrix.android.sdk.api.session.room.send.SendState + +object PollFixture { + + val A_MESSAGE_INFORMATION_DATA = MessageInformationData( + eventId = "eventId", + senderId = "senderId", + ageLocalTS = 0, + avatarUrl = "", + sendState = SendState.SENT, + messageLayout = TimelineMessageLayout.Default(showAvatar = true, showDisplayName = true, showTimestamp = true), + reactionsSummary = ReactionsSummaryData(), + sentByMe = true, + ) + + val A_POLL_RESPONSE_DATA = PollResponseData( + myVote = null, + votes = emptyMap(), + ) + + val A_POLL_OPTION_IDS = listOf("5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", "ec1a4db0-46d8-4d7a-9bb6-d80724715938", "3677ca8e-061b-40ab-bffe-b22e4e88fcad") + + val A_POLL_CONTENT = MessagePollContent( + unstablePollCreationInfo = PollCreationInfo( + question = PollQuestion( + unstableQuestion = "What is your favourite coffee?" + ), kind = PollType.UNDISCLOSED_UNSTABLE, maxSelections = 1, answers = listOf( + PollAnswer( + id = A_POLL_OPTION_IDS[0], unstableAnswer = "Double Espresso" + ), + PollAnswer( + id = A_POLL_OPTION_IDS[1], unstableAnswer = "Macchiato" + ), + PollAnswer( + id = A_POLL_OPTION_IDS[2], unstableAnswer = "Iced Coffee" + ), + ) + ) + ) +} diff --git a/vector/src/test/java/im/vector/app/test/fixtures/RoomPollFixture.kt b/vector/src/test/java/im/vector/app/test/fixtures/RoomPollFixture.kt deleted file mode 100644 index 4ccd9fa35a..0000000000 --- a/vector/src/test/java/im/vector/app/test/fixtures/RoomPollFixture.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.test.fixtures - -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState -import im.vector.app.features.roomprofile.polls.list.ui.PollSummary - -object RoomPollFixture { - - fun anActivePollSummary( - id: String = "", - timestamp: Long, - title: String = "", - ) = PollSummary.ActivePoll( - id = id, - creationTimestamp = timestamp, - title = title, - ) - - fun anEndedPollSummary( - id: String = "", - timestamp: Long, - title: String = "", - totalVotes: Int, - winnerOptions: List - ) = PollSummary.EndedPoll( - id = id, - creationTimestamp = timestamp, - title = title, - totalVotes = totalVotes, - winnerOptions = winnerOptions, - ) -} From 652a2c2834d02630a65e9d15dcca834339269832 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Wed, 25 Jan 2023 10:58:52 +0100 Subject: [PATCH 34/64] Fix migration of DB after rebase --- .../database/RealmSessionStoreMigration.kt | 4 +- .../database/migration/MigrateSessionTo050.kt | 43 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo050.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index fe55beb997..45bcd792c2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -66,6 +66,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo046 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo047 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo048 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo049 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo050 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -74,7 +75,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 49L, + schemaVersion = 50L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -133,5 +134,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 47) MigrateSessionTo047(realm).perform() if (oldVersion < 48) MigrateSessionTo048(realm).perform() if (oldVersion < 49) MigrateSessionTo049(realm).perform() + if (oldVersion < 50) MigrateSessionTo050(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo050.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo050.kt new file mode 100644 index 0000000000..1d864ae7e9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo050.kt @@ -0,0 +1,43 @@ +/* + * 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.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +/** + * Adding new entity PollHistoryStatusEntity. + */ +internal class MigrateSessionTo050(realm: DynamicRealm) : RealmMigrator(realm, 50) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.create("PollHistoryStatusEntity") + .addField(PollHistoryStatusEntityFields.ROOM_ID, String::class.java) + .addPrimaryKey(PollHistoryStatusEntityFields.ROOM_ID) + .setRequired(PollHistoryStatusEntityFields.ROOM_ID, true) + .addField(PollHistoryStatusEntityFields.CURRENT_TIMESTAMP_TARGET_BACKWARD_MS, Long::class.java) + .setNullable(PollHistoryStatusEntityFields.CURRENT_TIMESTAMP_TARGET_BACKWARD_MS, true) + .addField(PollHistoryStatusEntityFields.OLDEST_TIMESTAMP_TARGET_REACHED_MS, Long::class.java) + .setNullable(PollHistoryStatusEntityFields.OLDEST_TIMESTAMP_TARGET_REACHED_MS, true) + .addField(PollHistoryStatusEntityFields.OLDEST_EVENT_ID_REACHED, String::class.java) + .addField(PollHistoryStatusEntityFields.MOST_RECENT_EVENT_ID_REACHED, String::class.java) + .addField(PollHistoryStatusEntityFields.IS_END_OF_POLLS_BACKWARD, Boolean::class.java) + } +} + + From 8a54f7a4edea58ffb0a074f98a0bbf2ad67ecb0c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Wed, 25 Jan 2023 11:42:27 +0100 Subject: [PATCH 35/64] Revert nullable field in RoomAPI --- .../org/matrix/android/sdk/internal/session/room/RoomAPI.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index cf57e90c25..aa4bdb1dd4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -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?, From 7118368a15d063c0cb00f43b4837565b087dfef7 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Wed, 25 Jan 2023 11:47:46 +0100 Subject: [PATCH 36/64] Fix copyright in SDK --- .../android/sdk/api/session/room/poll/LoadedPollsStatus.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt index efc01e2cdf..003e03178b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 New Vector Ltd + * 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. From f9591a5fc622ee698997b3a8179cae87bf40a71f Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Wed, 25 Jan 2023 11:53:14 +0100 Subject: [PATCH 37/64] Fix code quality issues --- .../internal/database/migration/MigrateSessionTo050.kt | 2 -- .../sdk/internal/session/room/poll/LoadMorePollsTask.kt | 2 +- .../timeline/factory/PollItemViewStateFactoryTest.kt | 8 -------- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo050.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo050.kt index 1d864ae7e9..dfbfdc8da7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo050.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo050.kt @@ -39,5 +39,3 @@ internal class MigrateSessionTo050(realm: DynamicRealm) : RealmMigrator(realm, 5 .addField(PollHistoryStatusEntityFields.IS_END_OF_POLLS_BACKWARD, Boolean::class.java) } } - - 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 939769b525..248d1e5ee3 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 @@ -132,7 +132,7 @@ internal class DefaultLoadMorePollsTask @Inject constructor( status.oldestTimestampTargetReachedMs = oldestEventTimestamp } - if(oldestEventId != null) { + if (oldestEventId != null) { // save it for next backward pagination status.oldestEventIdReached = oldestEventId } diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt index 99c6c69849..512f7c8a17 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt @@ -17,12 +17,8 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.R -import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState -import im.vector.app.features.home.room.detail.timeline.item.PollResponseData import im.vector.app.features.home.room.detail.timeline.item.PollVoteSummaryData -import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryData -import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.poll.PollViewState import im.vector.app.test.fakes.FakeStringProvider import im.vector.app.test.fixtures.PollFixture.A_MESSAGE_INFORMATION_DATA @@ -34,10 +30,6 @@ 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.model.message.MessagePollContent -import org.matrix.android.sdk.api.session.room.model.message.PollAnswer -import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo -import org.matrix.android.sdk.api.session.room.model.message.PollQuestion import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.send.SendState From fc26d61305849a33ee8f9a40e758d6edee06b02e Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Thu, 26 Jan 2023 10:20:51 +0100 Subject: [PATCH 38/64] Removing a debug log --- .../sdk/internal/session/room/poll/DefaultPollHistoryService.kt | 2 -- 1 file changed, 2 deletions(-) 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 51c8b83191..28a857e6fa 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 @@ -38,7 +38,6 @@ 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 @@ -115,7 +114,6 @@ internal class DefaultPollHistoryService @AssistedInject constructor( return Transformations.switchMap(pollHistoryStatusLiveData) { results -> val oldestTimestamp = results.firstOrNull()?.oldestTimestampTargetReachedMs ?: clock.epochMillis() - Timber.d("oldestTimestamp=$oldestTimestamp") getPollStartEventsAfter(oldestTimestamp) } } From c7d3e1926f71c79f127a0fecb0fe39229343bded Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Thu, 26 Jan 2023 10:33:42 +0100 Subject: [PATCH 39/64] Renaming API field and adding more doc to make things clearer --- .../api/session/room/poll/LoadedPollsStatus.kt | 15 ++++++++++++++- .../api/session/room/poll/PollHistoryService.kt | 3 +++ .../session/room/poll/GetLoadedPollsStatusTask.kt | 2 +- .../session/room/poll/LoadMorePollsTask.kt | 2 +- .../roomprofile/polls/RoomPollsViewModel.kt | 4 ++-- .../roomprofile/polls/RoomPollsViewModelTest.kt | 8 ++++---- .../polls/list/data/RoomPollDataSourceTest.kt | 2 +- .../polls/list/data/RoomPollRepositoryTest.kt | 2 +- .../domain/GetLoadedPollsStatusUseCaseTest.kt | 2 +- .../polls/list/domain/LoadMorePollsUseCaseTest.kt | 2 +- .../polls/list/domain/SyncPollsUseCaseTest.kt | 6 +++--- 11 files changed, 32 insertions(+), 16 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt index 003e03178b..02a7667ebf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/poll/LoadedPollsStatus.kt @@ -20,7 +20,20 @@ package org.matrix.android.sdk.api.session.room.poll * Represent the status of the loaded polls for a room. */ data class LoadedPollsStatus( + /** + * Indicate whether more polls can be loaded from timeline. + * A false value would mean the start of the timeline has been reached. + */ val canLoadMore: Boolean, - val nbSyncedDays: Int, + + /** + * Number of days of timeline events currently synced (fetched and stored in local). + */ + val daysSynced: Int, + + /** + * Indicate whether a sync of timeline events has been completely done in backward. It would + * mean timeline events have been synced for at least a number of days defined by [PollHistoryService.loadingPeriodInDays]. + */ val hasCompletedASyncBackward: Boolean, ) 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 b62f5a1969..62706af86a 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 @@ -24,6 +24,9 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent */ interface PollHistoryService { + /** + * The number of days covered when requesting to load more polls. + */ val loadingPeriodInDays: Int /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.kt index f273d2248a..5bdb52d04c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/GetLoadedPollsStatusTask.kt @@ -43,7 +43,7 @@ internal class DefaultGetLoadedPollsStatusTask @Inject constructor( .copy() LoadedPollsStatus( canLoadMore = status.isEndOfPollsBackward.not(), - nbSyncedDays = status.getNbSyncedDays(params.currentTimestampMs), + daysSynced = status.getNbSyncedDays(params.currentTimestampMs), hasCompletedASyncBackward = status.hasCompletedASyncBackward, ) } 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 248d1e5ee3..50dbeb763e 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 @@ -53,7 +53,7 @@ internal class DefaultLoadMorePollsTask @Inject constructor( return LoadedPollsStatus( canLoadMore = currentPollHistoryStatus.isEndOfPollsBackward.not(), - nbSyncedDays = currentPollHistoryStatus.getNbSyncedDays(params.currentTimestampMs), + daysSynced = currentPollHistoryStatus.getNbSyncedDays(params.currentTimestampMs), hasCompletedASyncBackward = currentPollHistoryStatus.hasCompletedASyncBackward, ) } 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 488660f7d3..2beda47816 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 @@ -68,7 +68,7 @@ class RoomPollsViewModel @AssistedInject constructor( setState { copy( canLoadMore = loadedPollsStatus.canLoadMore, - nbSyncedDays = loadedPollsStatus.nbSyncedDays, + nbSyncedDays = loadedPollsStatus.daysSynced, ) } } @@ -100,7 +100,7 @@ class RoomPollsViewModel @AssistedInject constructor( setState { copy( canLoadMore = status.canLoadMore, - nbSyncedDays = status.nbSyncedDays, + nbSyncedDays = status.daysSynced, ) } } diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt index 084df03e64..20471637e6 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/RoomPollsViewModelTest.kt @@ -75,7 +75,7 @@ class RoomPollsViewModelTest { val expectedViewState = initialState.copy( polls = listOf(aPollSummary), canLoadMore = loadedPollsStatus.canLoadMore, - nbSyncedDays = loadedPollsStatus.nbSyncedDays, + nbSyncedDays = loadedPollsStatus.daysSynced, ) // When @@ -135,7 +135,7 @@ class RoomPollsViewModelTest { val stateAfterInit = initialState.copy( polls = emptyList(), canLoadMore = loadedPollsStatus.canLoadMore, - nbSyncedDays = loadedPollsStatus.nbSyncedDays, + nbSyncedDays = loadedPollsStatus.daysSynced, ) // When @@ -147,7 +147,7 @@ class RoomPollsViewModelTest { .assertStatesChanges( stateAfterInit, { copy(isLoadingMore = true) }, - { copy(canLoadMore = newLoadedPollsStatus.canLoadMore, nbSyncedDays = newLoadedPollsStatus.nbSyncedDays) }, + { copy(canLoadMore = newLoadedPollsStatus.canLoadMore, nbSyncedDays = newLoadedPollsStatus.daysSynced) }, { copy(isLoadingMore = false) }, ) .finish() @@ -181,7 +181,7 @@ class RoomPollsViewModelTest { private fun givenALoadedPollsStatus(canLoadMore: Boolean = true, nbSyncedDays: Int = 10) = LoadedPollsStatus( canLoadMore = canLoadMore, - nbSyncedDays = nbSyncedDays, + daysSynced = nbSyncedDays, hasCompletedASyncBackward = false, ) } diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSourceTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSourceTest.kt index 11006b10e8..89fde7b9df 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSourceTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollDataSourceTest.kt @@ -124,7 +124,7 @@ internal class RoomPollDataSourceTest { private fun givenALoadedPollsStatus() = LoadedPollsStatus( canLoadMore = true, - nbSyncedDays = 10, + daysSynced = 10, hasCompletedASyncBackward = true, ) } diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt index e57b52a812..f27335b844 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/data/RoomPollRepositoryTest.kt @@ -72,7 +72,7 @@ class RoomPollRepositoryTest { // Given val expectedStatus = LoadedPollsStatus( canLoadMore = true, - nbSyncedDays = 10, + daysSynced = 10, hasCompletedASyncBackward = false, ) coEvery { fakeRoomPollDataSource.getLoadedPollsStatus(A_ROOM_ID) } returns expectedStatus diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt index eeaf7803a6..2b3d731b3b 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/GetLoadedPollsStatusUseCaseTest.kt @@ -39,7 +39,7 @@ class GetLoadedPollsStatusUseCaseTest { val aRoomId = "roomId" val expectedStatus = LoadedPollsStatus( canLoadMore = true, - nbSyncedDays = 10, + daysSynced = 10, hasCompletedASyncBackward = true, ) coEvery { fakeRoomPollRepository.getLoadedPollsStatus(aRoomId) } returns expectedStatus diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt index d2f63c2a37..c1ae0a3a3f 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/LoadMorePollsUseCaseTest.kt @@ -39,7 +39,7 @@ class LoadMorePollsUseCaseTest { val aRoomId = "roomId" val loadedPollsStatus = LoadedPollsStatus( canLoadMore = true, - nbSyncedDays = 10, + daysSynced = 10, hasCompletedASyncBackward = true, ) coEvery { fakeRoomPollRepository.loadMorePolls(aRoomId) } returns loadedPollsStatus diff --git a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCaseTest.kt index e60214dde7..9dee8e6170 100644 --- a/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/roomprofile/polls/list/domain/SyncPollsUseCaseTest.kt @@ -45,7 +45,7 @@ class SyncPollsUseCaseTest { val aRoomId = "roomId" val aLoadedStatus = LoadedPollsStatus( canLoadMore = true, - nbSyncedDays = 10, + daysSynced = 10, hasCompletedASyncBackward = true, ) coJustRun { fakeRoomPollRepository.syncPolls(aRoomId) } @@ -71,12 +71,12 @@ class SyncPollsUseCaseTest { val aRoomId = "roomId" val aLoadedStatus = LoadedPollsStatus( canLoadMore = true, - nbSyncedDays = 10, + daysSynced = 10, hasCompletedASyncBackward = false, ) val anUpdatedLoadedStatus = LoadedPollsStatus( canLoadMore = true, - nbSyncedDays = 10, + daysSynced = 10, hasCompletedASyncBackward = true, ) coJustRun { fakeRoomPollRepository.syncPolls(aRoomId) } From 030e37655e82c12a51488395674399ccef1a2af9 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Thu, 26 Jan 2023 13:50:46 +0100 Subject: [PATCH 40/64] Fixing unit tests in SDK --- .../room/event/FilterAndStoreEventsTask.kt | 1 - .../DefaultFilterAndStoreEventsTaskTest.kt | 133 ++++++++++++++++++ .../DefaultFetchPollResponseEventsTaskTest.kt | 58 +++----- 3 files changed, 150 insertions(+), 42 deletions(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/event/DefaultFilterAndStoreEventsTaskTest.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt index eb7364abd3..e6e169b9b4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/event/FilterAndStoreEventsTask.kt @@ -58,7 +58,6 @@ internal class DefaultFilterAndStoreEventsTask @Inject constructor( private suspend fun addMissingEventsInDB(roomId: String, events: List) { monarchy.awaitTransaction { realm -> - // TODO we should insert TimelineEventEntity as well, how to do that???? val eventIdsToCheck = events.mapNotNull { it.eventId }.filter { it.isNotEmpty() } if (eventIdsToCheck.isNotEmpty()) { val existingIds = EventEntity.where(realm, eventIdsToCheck).findAll().toList().map { it.eventId } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/event/DefaultFilterAndStoreEventsTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/event/DefaultFilterAndStoreEventsTaskTest.kt new file mode 100644 index 0000000000..1b45430fd1 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/event/DefaultFilterAndStoreEventsTaskTest.kt @@ -0,0 +1,133 @@ +/* + * 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.event + +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.isPollResponse +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventEntityFields +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse +import org.matrix.android.sdk.internal.session.room.relation.poll.FETCH_RELATED_EVENTS_LIMIT +import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask +import org.matrix.android.sdk.test.fakes.FakeClock +import org.matrix.android.sdk.test.fakes.FakeEventDecryptor +import org.matrix.android.sdk.test.fakes.FakeMonarchy +import org.matrix.android.sdk.test.fakes.givenFindAll +import org.matrix.android.sdk.test.fakes.givenIn + +@OptIn(ExperimentalCoroutinesApi::class) +internal class DefaultFilterAndStoreEventsTaskTest { + + private val fakeMonarchy = FakeMonarchy() + private val fakeClock = FakeClock() + private val fakeEventDecryptor = FakeEventDecryptor() + + private val defaultFilterAndStoreEventsTask = DefaultFilterAndStoreEventsTask( + monarchy = fakeMonarchy.instance, + clock = fakeClock, + eventDecryptor = fakeEventDecryptor.instance, + ) + + @Before + fun setup() { + mockkStatic("org.matrix.android.sdk.api.session.events.model.EventKt") + mockkStatic("org.matrix.android.sdk.internal.database.mapper.EventMapperKt") + mockkStatic("org.matrix.android.sdk.internal.database.query.EventEntityQueriesKt") + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given a room and list of events when execute then filter in using given predicate and store them in local if needed`() = runTest { + // Given + val aRoomId = "roomId" + val anEventId1 = "eventId1" + val anEventId2 = "eventId2" + val anEventId3 = "eventId3" + val anEventId4 = "eventId4" + val event1 = givenAnEvent(eventId = anEventId1, isEncrypted = true, clearType = EventType.ENCRYPTED) + val event2 = givenAnEvent(eventId = anEventId2, isEncrypted = true, clearType = EventType.MESSAGE) + val event3 = givenAnEvent(eventId = anEventId3, isEncrypted = false, clearType = EventType.MESSAGE) + val event4 = givenAnEvent(eventId = anEventId4, isEncrypted = false, clearType = EventType.MESSAGE) + val events = listOf(event1, event2, event3, event4) + val filterPredicate = { event: Event -> event == event2 } + val params = givenTaskParams(roomId = aRoomId, events = events, predicate = filterPredicate) + fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event1) + fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event2) + fakeClock.givenEpoch(123) + givenExistingEventEntities(eventIdsToCheck = listOf(anEventId1, anEventId2), existingIds = listOf(anEventId1)) + val eventEntityToSave = EventEntity(eventId = anEventId2) + every { event2.toEntity(any(), any(), any()) } returns eventEntityToSave + every { eventEntityToSave.copyToRealmOrIgnore(any(), any()) } returns eventEntityToSave + + // When + defaultFilterAndStoreEventsTask.execute(params) + + // Then + fakeEventDecryptor.verifyDecryptEventAndSaveResult(event1, timeline = "") + fakeEventDecryptor.verifyDecryptEventAndSaveResult(event2, timeline = "") + // Check we save in DB the event2 which is a non stored poll response + verify { + event2.toEntity(aRoomId, SendState.SYNCED, any()) + eventEntityToSave.copyToRealmOrIgnore(fakeMonarchy.fakeRealm.instance, EventInsertType.PAGINATION) + } + } + + private fun givenTaskParams(roomId: String, events: List, predicate: (Event) -> Boolean) = FilterAndStoreEventsTask.Params( + roomId = roomId, + events = events, + filterPredicate = predicate, + ) + + private fun givenAnEvent( + eventId: String, + isEncrypted: Boolean, + clearType: String, + ): Event { + val event = mockk(relaxed = true) + every { event.eventId } returns eventId + every { event.isEncrypted() } returns isEncrypted + every { event.getClearType() } returns clearType + return event + } + + private fun givenExistingEventEntities(eventIdsToCheck: List, existingIds: List) { + val eventEntities = existingIds.map { EventEntity(eventId = it) } + fakeMonarchy.givenWhere() + .givenIn(EventEntityFields.EVENT_ID, eventIdsToCheck) + .givenFindAll(eventEntities) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt index 8d50bac38f..238a4fa626 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/relation/poll/DefaultFetchPollResponseEventsTaskTest.kt @@ -16,11 +16,12 @@ package org.matrix.android.sdk.internal.session.room.relation.poll +import io.mockk.coJustRun +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.unmockkAll -import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.After @@ -29,41 +30,28 @@ import org.junit.Test import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.isPollResponse -import org.matrix.android.sdk.api.session.room.send.SendState -import org.matrix.android.sdk.internal.database.mapper.toEntity -import org.matrix.android.sdk.internal.database.model.EventEntity -import org.matrix.android.sdk.internal.database.model.EventEntityFields -import org.matrix.android.sdk.internal.database.model.EventInsertType -import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.session.room.event.FilterAndStoreEventsTask import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse -import org.matrix.android.sdk.test.fakes.FakeClock -import org.matrix.android.sdk.test.fakes.FakeEventDecryptor import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver -import org.matrix.android.sdk.test.fakes.FakeMonarchy import org.matrix.android.sdk.test.fakes.FakeRoomApi -import org.matrix.android.sdk.test.fakes.givenFindAll -import org.matrix.android.sdk.test.fakes.givenIn @OptIn(ExperimentalCoroutinesApi::class) internal class DefaultFetchPollResponseEventsTaskTest { private val fakeRoomAPI = FakeRoomApi() private val fakeGlobalErrorReceiver = FakeGlobalErrorReceiver() - private val fakeMonarchy = FakeMonarchy() - private val fakeClock = FakeClock() - private val fakeEventDecryptor = FakeEventDecryptor() + private val filterAndStoreEventsTask = mockk() private val defaultFetchPollResponseEventsTask = DefaultFetchPollResponseEventsTask( roomAPI = fakeRoomAPI.instance, globalErrorReceiver = fakeGlobalErrorReceiver, - monarchy = fakeMonarchy.instance, - clock = fakeClock, - eventDecryptor = fakeEventDecryptor.instance, + filterAndStoreEventsTask = filterAndStoreEventsTask, ) @Before fun setup() { mockkStatic("org.matrix.android.sdk.api.session.events.model.EventKt") + mockkStatic("org.matrix.android.sdk.internal.database.mapper.EventMapperKt") mockkStatic("org.matrix.android.sdk.internal.database.query.EventEntityQueriesKt") } @@ -74,7 +62,7 @@ internal class DefaultFetchPollResponseEventsTaskTest { } @Test - fun `given a room and a poll when execute then fetch related events and store them in local if needed`() = runTest { + fun `given a room and a poll when execute then fetch related events and store them in local`() = runTest { // Given val aRoomId = "roomId" val aPollEventId = "eventId" @@ -94,13 +82,7 @@ internal class DefaultFetchPollResponseEventsTaskTest { fakeRoomAPI.givenGetRelationsReturns(from = null, relationsResponse = firstResponse) val secondResponse = givenARelationsResponse(events = secondEvents, nextBatch = null) fakeRoomAPI.givenGetRelationsReturns(from = aNextBatchToken, relationsResponse = secondResponse) - fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event1) - fakeEventDecryptor.givenDecryptEventAndSaveResultSuccess(event2) - fakeClock.givenEpoch(123) - givenExistingEventEntities(eventIdsToCheck = listOf(anEventId1, anEventId2), existingIds = listOf(anEventId1)) - val eventEntityToSave = EventEntity(eventId = anEventId2) - every { event2.toEntity(any(), any(), any()) } returns eventEntityToSave - every { eventEntityToSave.copyToRealmOrIgnore(any(), any()) } returns eventEntityToSave + coJustRun { filterAndStoreEventsTask.execute(any()) } // When defaultFetchPollResponseEventsTask.execute(params) @@ -111,21 +93,22 @@ internal class DefaultFetchPollResponseEventsTaskTest { eventId = params.startPollEventId, relationType = RelationType.REFERENCE, from = null, - limit = FETCH_RELATED_EVENTS_LIMIT + limit = FETCH_RELATED_EVENTS_LIMIT, ) fakeRoomAPI.verifyGetRelations( roomId = params.roomId, eventId = params.startPollEventId, relationType = RelationType.REFERENCE, from = aNextBatchToken, - limit = FETCH_RELATED_EVENTS_LIMIT + limit = FETCH_RELATED_EVENTS_LIMIT, ) - fakeEventDecryptor.verifyDecryptEventAndSaveResult(event1, timeline = "") - fakeEventDecryptor.verifyDecryptEventAndSaveResult(event2, timeline = "") - // Check we save in DB the event2 which is a non stored poll response - verify { - event2.toEntity(aRoomId, SendState.SYNCED, any()) - eventEntityToSave.copyToRealmOrIgnore(fakeMonarchy.fakeRealm.instance, EventInsertType.PAGINATION) + coVerify { + filterAndStoreEventsTask.execute(match { + it.roomId == aRoomId && it.events == firstEvents + }) + filterAndStoreEventsTask.execute(match { + it.roomId == aRoomId && it.events == secondEvents + }) } } @@ -153,11 +136,4 @@ internal class DefaultFetchPollResponseEventsTaskTest { every { event.isEncrypted() } returns isEncrypted return event } - - private fun givenExistingEventEntities(eventIdsToCheck: List, existingIds: List) { - val eventEntities = existingIds.map { EventEntity(eventId = it) } - fakeMonarchy.givenWhere() - .givenIn(EventEntityFields.EVENT_ID, eventIdsToCheck) - .givenFindAll(eventEntities) - } } From 3045a8581ad645f62c267a44d8a0be1e79248ff9 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Thu, 26 Jan 2023 13:58:30 +0100 Subject: [PATCH 41/64] Adding unit tests for DefaultGetLoadedPollsStatusTaskTest --- .../DefaultGetLoadedPollsStatusTaskTest.kt | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultGetLoadedPollsStatusTaskTest.kt diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultGetLoadedPollsStatusTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultGetLoadedPollsStatusTaskTest.kt new file mode 100644 index 0000000000..2f58973eca --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultGetLoadedPollsStatusTaskTest.kt @@ -0,0 +1,121 @@ +/* + * 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 + +import io.mockk.unmockkAll +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Test +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.model.PollHistoryStatusEntityFields +import org.matrix.android.sdk.test.fakes.FakeMonarchy +import org.matrix.android.sdk.test.fakes.givenEqualTo +import org.matrix.android.sdk.test.fakes.givenFindFirst + +private const val A_ROOM_ID = "room-id" +/** + * 2023/01/26 + */ +private const val A_TIMESTAMP = 1674737619290L + +@OptIn(ExperimentalCoroutinesApi::class) +internal class DefaultGetLoadedPollsStatusTaskTest { + + private val fakeMonarchy = FakeMonarchy() + + private val defaultGetLoadedPollsStatusTask = DefaultGetLoadedPollsStatusTask( + monarchy = fakeMonarchy.instance, + ) + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given poll history status exists in db with an oldestTimestamp reached when execute then the computed status is returned`() = runTest { + // Given + val params = givenTaskParams() + // 2023/01/20 + val oldestTimestampReached = 1674169200000 + val pollHistoryStatus = aPollHistoryStatusEntity( + isEndOfPollsBackward = false, + oldestTimestampReached = oldestTimestampReached, + ) + fakeMonarchy.fakeRealm + .givenWhere() + .givenEqualTo(PollHistoryStatusEntityFields.ROOM_ID, A_ROOM_ID) + .givenFindFirst(pollHistoryStatus) + val expectedStatus = LoadedPollsStatus( + canLoadMore = true, + daysSynced = 6, + hasCompletedASyncBackward = true, + ) + + // When + val result = defaultGetLoadedPollsStatusTask.execute(params) + + // Then + result shouldBeEqualTo expectedStatus + } + + @Test + fun `given poll history status exists in db and no oldestTimestamp reached when execute then the computed status is returned`() = runTest { + // Given + val params = givenTaskParams() + val pollHistoryStatus = aPollHistoryStatusEntity( + isEndOfPollsBackward = false, + oldestTimestampReached = null, + ) + fakeMonarchy.fakeRealm + .givenWhere() + .givenEqualTo(PollHistoryStatusEntityFields.ROOM_ID, A_ROOM_ID) + .givenFindFirst(pollHistoryStatus) + val expectedStatus = LoadedPollsStatus( + canLoadMore = true, + daysSynced = 0, + hasCompletedASyncBackward = false, + ) + + // When + val result = defaultGetLoadedPollsStatusTask.execute(params) + + // Then + result shouldBeEqualTo expectedStatus + } + + private fun givenTaskParams(): GetLoadedPollsStatusTask.Params { + return GetLoadedPollsStatusTask.Params( + roomId = A_ROOM_ID, + currentTimestampMs = A_TIMESTAMP, + ) + } + + private fun aPollHistoryStatusEntity( + isEndOfPollsBackward: Boolean, + oldestTimestampReached: Long?, + ): PollHistoryStatusEntity { + return PollHistoryStatusEntity( + roomId = A_ROOM_ID, + isEndOfPollsBackward = isEndOfPollsBackward, + oldestTimestampTargetReachedMs = oldestTimestampReached, + ) + } +} From 41825812352f6cf07acabab983c65205dc095698 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Thu, 26 Jan 2023 14:51:16 +0100 Subject: [PATCH 42/64] Adding unit tests for SyncPollsTask --- .../session/room/poll/SyncPollsTask.kt | 9 +- .../room/poll/DefaultSyncPollsTaskTest.kt | 129 ++++++++++++++++++ .../android/sdk/test/fakes/FakeTimeline.kt | 40 ++++++ 3 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultSyncPollsTaskTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeTimeline.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/SyncPollsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/SyncPollsTask.kt index 4a83f0f870..fff24288b4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/SyncPollsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/poll/SyncPollsTask.kt @@ -84,18 +84,17 @@ internal class DefaultSyncPollsTask @Inject constructor( ): LoadStatus { return monarchy.awaitTransaction { realm -> val status = PollHistoryStatusEntity.getOrCreate(realm, roomId) - val mostRecentEventIdReached = status.mostRecentEventIdReached - val mostRecentEvent = events .maxByOrNull { it.root.originServerTs ?: Long.MIN_VALUE } ?.root + val mostRecentEventIdReached = mostRecentEvent?.eventId - if (mostRecentEventIdReached == null) { + if (mostRecentEventIdReached != null) { // save it for next forward pagination - status.mostRecentEventIdReached = mostRecentEvent?.eventId + status.mostRecentEventIdReached = mostRecentEventIdReached } - val mostRecentTimestamp = mostRecentEvent?.ageLocalTs + val mostRecentTimestamp = mostRecentEvent?.originServerTs val shouldLoadMore = paginationState.hasMoreToLoad && (mostRecentTimestamp == null || mostRecentTimestamp < currentTimestampMs) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultSyncPollsTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultSyncPollsTaskTest.kt new file mode 100644 index 0000000000..8a95a2f131 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultSyncPollsTaskTest.kt @@ -0,0 +1,129 @@ +/* + * 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 + +import io.mockk.coVerifyOrder +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Test +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntity +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntityFields +import org.matrix.android.sdk.test.fakes.FakeMonarchy +import org.matrix.android.sdk.test.fakes.FakeTimeline +import org.matrix.android.sdk.test.fakes.givenEqualTo +import org.matrix.android.sdk.test.fakes.givenFindFirst + +private const val A_ROOM_ID = "room-id" +private const val A_TIMESTAMP = 123L +private const val A_PAGE_SIZE = 200 + +@OptIn(ExperimentalCoroutinesApi::class) +internal class DefaultSyncPollsTaskTest { + + private val fakeMonarchy = FakeMonarchy() + private val fakeTimeline = FakeTimeline() + + private val defaultSyncPollsTask = DefaultSyncPollsTask( + monarchy = fakeMonarchy.instance, + ) + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given timeline when execute then more events are fetched in forward direction after the most recent event id reached`() = runTest { + // Given + val params = givenTaskParams() + val mostRecentEventId = "most-recent" + val oldestEventId = "oldest" + val pollHistoryStatus = aPollHistoryStatusEntity( + mostRecentEventIdReached = mostRecentEventId, + oldestEventIdReached = oldestEventId, + ) + fakeMonarchy.fakeRealm + .givenWhere() + .givenEqualTo(PollHistoryStatusEntityFields.ROOM_ID, A_ROOM_ID) + .givenFindFirst(pollHistoryStatus) + fakeTimeline.givenRestartWithEventIdSuccess(mostRecentEventId) + fakeTimeline.givenRestartWithEventIdSuccess(oldestEventId) + val anEventId = "event-id" + val aTimelineEvent = aTimelineEvent(anEventId) + fakeTimeline.givenAwaitPaginateReturns( + events = listOf(aTimelineEvent), + direction = Timeline.Direction.FORWARDS, + count = params.eventsPageSize, + ) + fakeTimeline.givenGetPaginationStateReturns( + paginationState = aPaginationState(), + direction = Timeline.Direction.FORWARDS, + ) + + // When + defaultSyncPollsTask.execute(params) + + // Then + coVerifyOrder { + fakeTimeline.instance.restartWithEventId(mostRecentEventId) + fakeTimeline.instance.awaitPaginate(direction = Timeline.Direction.FORWARDS, count = params.eventsPageSize) + fakeTimeline.instance.getPaginationState(direction = Timeline.Direction.FORWARDS) + fakeTimeline.instance.restartWithEventId(oldestEventId) + } + pollHistoryStatus.mostRecentEventIdReached shouldBeEqualTo anEventId + } + + private fun givenTaskParams(): SyncPollsTask.Params { + return SyncPollsTask.Params( + timeline = fakeTimeline.instance, + roomId = A_ROOM_ID, + currentTimestampMs = A_TIMESTAMP, + eventsPageSize = A_PAGE_SIZE, + ) + } + + private fun aPollHistoryStatusEntity( + mostRecentEventIdReached: String, + oldestEventIdReached: String, + ): PollHistoryStatusEntity { + return PollHistoryStatusEntity( + roomId = A_ROOM_ID, + mostRecentEventIdReached = mostRecentEventIdReached, + oldestEventIdReached = oldestEventIdReached, + ) + } + + private fun aTimelineEvent(eventId: String): TimelineEvent { + val event = mockk() + every { event.root.originServerTs } returns 123L + every { event.root.eventId } returns eventId + return event + } + + private fun aPaginationState(): Timeline.PaginationState { + return Timeline.PaginationState( + hasMoreToLoad = false, + ) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeTimeline.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeTimeline.kt new file mode 100644 index 0000000000..68b80c7e8f --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeTimeline.kt @@ -0,0 +1,40 @@ +/* + * 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.test.fakes + +import io.mockk.coEvery +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +class FakeTimeline { + val instance: Timeline = mockk() + + fun givenRestartWithEventIdSuccess(eventId: String) { + justRun { instance.restartWithEventId(eventId) } + } + + fun givenAwaitPaginateReturns(events: List, direction: Timeline.Direction, count: Int) { + coEvery { instance.awaitPaginate(direction, count) } returns events + } + + fun givenGetPaginationStateReturns(paginationState: Timeline.PaginationState, direction: Timeline.Direction) { + every { instance.getPaginationState(direction) } returns paginationState + } +} From b6f77ac578ef8d2c453f0c33b367d67df1fa0a2c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Thu, 26 Jan 2023 15:38:11 +0100 Subject: [PATCH 43/64] Adding unit tests for LoadMorePollsTask --- .../DefaultFilterAndStoreEventsTaskTest.kt | 5 - .../DefaultGetLoadedPollsStatusTaskTest.kt | 16 +- .../room/poll/DefaultLoadMorePollsTaskTest.kt | 192 ++++++++++++++++++ 3 files changed, 202 insertions(+), 11 deletions(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultLoadMorePollsTaskTest.kt diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/event/DefaultFilterAndStoreEventsTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/event/DefaultFilterAndStoreEventsTaskTest.kt index 1b45430fd1..81e43c7c03 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/event/DefaultFilterAndStoreEventsTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/event/DefaultFilterAndStoreEventsTaskTest.kt @@ -28,17 +28,12 @@ import org.junit.Before import org.junit.Test import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.isPollResponse import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntityFields import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore -import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse -import org.matrix.android.sdk.internal.session.room.relation.poll.FETCH_RELATED_EVENTS_LIMIT -import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollResponseEventsTask import org.matrix.android.sdk.test.fakes.FakeClock import org.matrix.android.sdk.test.fakes.FakeEventDecryptor import org.matrix.android.sdk.test.fakes.FakeMonarchy diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultGetLoadedPollsStatusTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultGetLoadedPollsStatusTaskTest.kt index 2f58973eca..9c3093897d 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultGetLoadedPollsStatusTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultGetLoadedPollsStatusTaskTest.kt @@ -30,10 +30,16 @@ import org.matrix.android.sdk.test.fakes.givenEqualTo import org.matrix.android.sdk.test.fakes.givenFindFirst private const val A_ROOM_ID = "room-id" + /** - * 2023/01/26 + * Timestamp in milliseconds corresponding to 2023/01/26. */ -private const val A_TIMESTAMP = 1674737619290L +private const val A_CURRENT_TIMESTAMP = 1674737619290L + +/** + * Timestamp in milliseconds corresponding to 2023/01/20. + */ +private const val AN_EVENT_TIMESTAMP = 1674169200000L @OptIn(ExperimentalCoroutinesApi::class) internal class DefaultGetLoadedPollsStatusTaskTest { @@ -53,11 +59,9 @@ internal class DefaultGetLoadedPollsStatusTaskTest { fun `given poll history status exists in db with an oldestTimestamp reached when execute then the computed status is returned`() = runTest { // Given val params = givenTaskParams() - // 2023/01/20 - val oldestTimestampReached = 1674169200000 val pollHistoryStatus = aPollHistoryStatusEntity( isEndOfPollsBackward = false, - oldestTimestampReached = oldestTimestampReached, + oldestTimestampReached = AN_EVENT_TIMESTAMP, ) fakeMonarchy.fakeRealm .givenWhere() @@ -104,7 +108,7 @@ internal class DefaultGetLoadedPollsStatusTaskTest { private fun givenTaskParams(): GetLoadedPollsStatusTask.Params { return GetLoadedPollsStatusTask.Params( roomId = A_ROOM_ID, - currentTimestampMs = A_TIMESTAMP, + currentTimestampMs = A_CURRENT_TIMESTAMP, ) } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultLoadMorePollsTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultLoadMorePollsTaskTest.kt new file mode 100644 index 0000000000..489a32b198 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/poll/DefaultLoadMorePollsTaskTest.kt @@ -0,0 +1,192 @@ +/* + * 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 + +import io.mockk.coVerifyOrder +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Test +import org.matrix.android.sdk.api.session.room.poll.LoadedPollsStatus +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntity +import org.matrix.android.sdk.internal.database.model.PollHistoryStatusEntityFields +import org.matrix.android.sdk.test.fakes.FakeMonarchy +import org.matrix.android.sdk.test.fakes.FakeTimeline +import org.matrix.android.sdk.test.fakes.givenEqualTo +import org.matrix.android.sdk.test.fakes.givenFindFirst + +private const val A_ROOM_ID = "room-id" + +/** + * Timestamp in milliseconds corresponding to 2023/01/26. + */ +private const val A_CURRENT_TIMESTAMP = 1674737619290L + +/** + * Timestamp in milliseconds corresponding to 2023/01/20. + */ +private const val AN_EVENT_TIMESTAMP = 1674169200000L +private const val A_PERIOD_IN_DAYS = 3 +private const val A_PAGE_SIZE = 200 + +@OptIn(ExperimentalCoroutinesApi::class) +internal class DefaultLoadMorePollsTaskTest { + + private val fakeMonarchy = FakeMonarchy() + private val fakeTimeline = FakeTimeline() + + private val defaultLoadMorePollsTask = DefaultLoadMorePollsTask( + monarchy = fakeMonarchy.instance, + ) + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given timeline when execute then more events are fetched in backward direction until has no more to load`() = runTest { + // Given + val params = givenTaskParams() + val oldestEventId = "oldest" + val pollHistoryStatus = aPollHistoryStatusEntity( + oldestEventIdReached = oldestEventId, + ) + fakeMonarchy.fakeRealm + .givenWhere() + .givenEqualTo(PollHistoryStatusEntityFields.ROOM_ID, A_ROOM_ID) + .givenFindFirst(pollHistoryStatus) + fakeTimeline.givenRestartWithEventIdSuccess(oldestEventId) + val anEventId = "event-id" + val aTimelineEvent = aTimelineEvent(anEventId, AN_EVENT_TIMESTAMP) + fakeTimeline.givenAwaitPaginateReturns( + events = listOf(aTimelineEvent), + direction = Timeline.Direction.BACKWARDS, + count = params.eventsPageSize, + ) + val aPaginationState = aPaginationState(hasMoreToLoad = false) + fakeTimeline.givenGetPaginationStateReturns( + paginationState = aPaginationState, + direction = Timeline.Direction.BACKWARDS, + ) + val expectedLoadStatus = LoadedPollsStatus( + canLoadMore = false, + daysSynced = 6, + hasCompletedASyncBackward = true, + ) + + // When + val result = defaultLoadMorePollsTask.execute(params) + + // Then + coVerifyOrder { + fakeTimeline.instance.restartWithEventId(oldestEventId) + fakeTimeline.instance.awaitPaginate(direction = Timeline.Direction.BACKWARDS, count = params.eventsPageSize) + fakeTimeline.instance.getPaginationState(direction = Timeline.Direction.BACKWARDS) + } + pollHistoryStatus.mostRecentEventIdReached shouldBeEqualTo anEventId + pollHistoryStatus.oldestEventIdReached shouldBeEqualTo anEventId + pollHistoryStatus.isEndOfPollsBackward shouldBeEqualTo true + pollHistoryStatus.oldestTimestampTargetReachedMs shouldBeEqualTo AN_EVENT_TIMESTAMP + result shouldBeEqualTo expectedLoadStatus + } + + @Test + fun `given timeline when execute then more events are fetched in backward direction until current target is reached`() = runTest { + // Given + val params = givenTaskParams() + val oldestEventId = "oldest" + val pollHistoryStatus = aPollHistoryStatusEntity( + oldestEventIdReached = oldestEventId, + ) + fakeMonarchy.fakeRealm + .givenWhere() + .givenEqualTo(PollHistoryStatusEntityFields.ROOM_ID, A_ROOM_ID) + .givenFindFirst(pollHistoryStatus) + fakeTimeline.givenRestartWithEventIdSuccess(oldestEventId) + val anEventId = "event-id" + val aTimelineEvent = aTimelineEvent(anEventId, AN_EVENT_TIMESTAMP) + fakeTimeline.givenAwaitPaginateReturns( + events = listOf(aTimelineEvent), + direction = Timeline.Direction.BACKWARDS, + count = params.eventsPageSize, + ) + val aPaginationState = aPaginationState(hasMoreToLoad = true) + fakeTimeline.givenGetPaginationStateReturns( + paginationState = aPaginationState, + direction = Timeline.Direction.BACKWARDS, + ) + val expectedLoadStatus = LoadedPollsStatus( + canLoadMore = true, + daysSynced = 6, + hasCompletedASyncBackward = true, + ) + + // When + val result = defaultLoadMorePollsTask.execute(params) + + // Then + coVerifyOrder { + fakeTimeline.instance.restartWithEventId(oldestEventId) + fakeTimeline.instance.awaitPaginate(direction = Timeline.Direction.BACKWARDS, count = params.eventsPageSize) + fakeTimeline.instance.getPaginationState(direction = Timeline.Direction.BACKWARDS) + } + pollHistoryStatus.mostRecentEventIdReached shouldBeEqualTo anEventId + pollHistoryStatus.oldestEventIdReached shouldBeEqualTo anEventId + pollHistoryStatus.isEndOfPollsBackward shouldBeEqualTo false + pollHistoryStatus.oldestTimestampTargetReachedMs shouldBeEqualTo AN_EVENT_TIMESTAMP + result shouldBeEqualTo expectedLoadStatus + } + + private fun givenTaskParams(): LoadMorePollsTask.Params { + return LoadMorePollsTask.Params( + timeline = fakeTimeline.instance, + roomId = A_ROOM_ID, + currentTimestampMs = A_CURRENT_TIMESTAMP, + loadingPeriodInDays = A_PERIOD_IN_DAYS, + eventsPageSize = A_PAGE_SIZE, + ) + } + + private fun aPollHistoryStatusEntity( + oldestEventIdReached: String, + ): PollHistoryStatusEntity { + return PollHistoryStatusEntity( + roomId = A_ROOM_ID, + oldestEventIdReached = oldestEventIdReached, + ) + } + + private fun aTimelineEvent(eventId: String, timestamp: Long): TimelineEvent { + val event = mockk() + every { event.root.originServerTs } returns timestamp + every { event.root.eventId } returns eventId + return event + } + + private fun aPaginationState(hasMoreToLoad: Boolean): Timeline.PaginationState { + return Timeline.PaginationState( + hasMoreToLoad = hasMoreToLoad, + ) + } +} From 25a09bc44612e582fd7fe37ca36b4a8df655ade6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Jan 2023 13:29:00 +0100 Subject: [PATCH 44/64] Add a debug slash command to crash the application from the timeline screen. --- library/ui-strings/src/main/res/values/donottranslate.xml | 2 ++ .../main/java/im/vector/app/features/command/Command.kt | 1 + .../java/im/vector/app/features/command/CommandParser.kt | 8 +++++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values/donottranslate.xml b/library/ui-strings/src/main/res/values/donottranslate.xml index bfe751ef5a..910ce31c41 100755 --- a/library/ui-strings/src/main/res/values/donottranslate.xml +++ b/library/ui-strings/src/main/res/values/donottranslate.xml @@ -10,6 +10,8 @@ Cut the slack from teams. + Crash the application. + © MapTiler © OpenStreetMap contributors diff --git a/vector/src/main/java/im/vector/app/features/command/Command.kt b/vector/src/main/java/im/vector/app/features/command/Command.kt index 324029c45b..b112751f68 100644 --- a/vector/src/main/java/im/vector/app/features/command/Command.kt +++ b/vector/src/main/java/im/vector/app/features/command/Command.kt @@ -32,6 +32,7 @@ enum class Command( val isDevCommand: Boolean, val isThreadCommand: Boolean ) { + CRASH_APP("/crash", null, "", R.string.command_description_crash_application, true, true), EMOTE("/me", null, "", R.string.command_description_emote, false, true), BAN_USER("/ban", null, " [reason]", R.string.command_description_ban_user, false, false), UNBAN_USER("/unban", null, " [reason]", R.string.command_description_unban_user, false, false), diff --git a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt index e08bc9fb64..298387c324 100644 --- a/vector/src/main/java/im/vector/app/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/app/features/command/CommandParser.kt @@ -20,13 +20,16 @@ import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.isMsisdn import im.vector.app.core.extensions.orEmpty import im.vector.app.features.home.room.detail.ChatEffect +import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl import org.matrix.android.sdk.api.session.identity.ThreePid import timber.log.Timber import javax.inject.Inject -class CommandParser @Inject constructor() { +class CommandParser @Inject constructor( + private val vectorPreferences: VectorPreferences +) { /** * Convert the text message into a Slash command. @@ -404,6 +407,9 @@ class CommandParser @Inject constructor() { ParsedCommand.ErrorSyntax(Command.UPGRADE_ROOM) } } + Command.CRASH_APP.matches(slashCommand) && vectorPreferences.developerMode() -> { + throw RuntimeException("Application crashed from user demand") + } else -> { // Unknown command ParsedCommand.ErrorUnknownSlashCommand(slashCommand) From 8f927a46ca14aaf317fc60d508c1710051659723 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Jan 2023 15:41:23 +0100 Subject: [PATCH 45/64] Fix issue of `Idle` displayed after pausing and resuming the app. --- .../src/main/java/org/matrix/android/sdk/flow/FlowSession.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt index 80ed311901..64cb0acb2d 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowSession.kt @@ -80,6 +80,9 @@ class FlowSession(private val session: Session) { fun liveSyncState(): Flow { return session.syncService().getSyncStateLive().asFlow() + .startWith(session.coroutineDispatchers.io) { + session.syncService().getSyncState() + } } fun livePushers(): Flow> { From fae17840055a5b57bee0ad926788c25bacd7a591 Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Thu, 26 Jan 2023 19:17:02 +0000 Subject: [PATCH 46/64] Translated using Weblate (Czech) Currently translated at 100.0% (2597 of 2597 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/cs/ --- library/ui-strings/src/main/res/values-cs/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-cs/strings.xml b/library/ui-strings/src/main/res/values-cs/strings.xml index c9d697f560..c122de7798 100644 --- a/library/ui-strings/src/main/res/values-cs/strings.xml +++ b/library/ui-strings/src/main/res/values-cs/strings.xml @@ -2978,4 +2978,5 @@ Hlasovou zprávu nelze spustit, protože právě nahráváte živé vysílání. Ukončete prosím živé vysílání, abyste mohli začít nahrávat hlasovou zprávu Nelze spustit hlasovou zprávu - + Chyba připojení - nahrávání pozastaveno + \ No newline at end of file From 5f33474ff5c6fac5ece986dd8d3416bc2d7b8d23 Mon Sep 17 00:00:00 2001 From: Vri Date: Wed, 25 Jan 2023 12:21:26 +0000 Subject: [PATCH 47/64] Translated using Weblate (German) Currently translated at 100.0% (2597 of 2597 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/de/ --- library/ui-strings/src/main/res/values-de/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-de/strings.xml b/library/ui-strings/src/main/res/values-de/strings.xml index f0e5a7bb8d..17fa9b6e44 100644 --- a/library/ui-strings/src/main/res/values-de/strings.xml +++ b/library/ui-strings/src/main/res/values-de/strings.xml @@ -2917,4 +2917,5 @@ Du kannst keine Sprachnachricht beginnen, da du im Moment eine Echtzeitübertragung aufzeichnest. Bitte beende deine Sprachübertragung, um ein Gespräch zu beginnen Kann Sprachnachricht nicht beginnen - + Verbindungsfehler − Aufnahme pausiert + \ No newline at end of file From e971e09e2e48be86d9b185a3c8b2f30891da2c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Wed, 25 Jan 2023 19:01:18 +0000 Subject: [PATCH 48/64] Translated using Weblate (Estonian) Currently translated at 99.6% (2589 of 2597 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/et/ --- library/ui-strings/src/main/res/values-et/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-et/strings.xml b/library/ui-strings/src/main/res/values-et/strings.xml index 1d7b96d2f9..f33ade2a7e 100644 --- a/library/ui-strings/src/main/res/values-et/strings.xml +++ b/library/ui-strings/src/main/res/values-et/strings.xml @@ -2909,4 +2909,5 @@ Viga küsitluste laadimisel. Häälsõnumi esitamine ei õnnestu Kuna sa hetkel salvestad ringhäälingukõnet, siis häälsõnumi salvestamine või esitamine ei õnnestu. Selleks palun lõpeta ringhäälingukõne - + Viga võrguühenduses - salvestamine on peatatud + \ No newline at end of file From c868452194663f097a04af0a1c165e2f7ac2102d Mon Sep 17 00:00:00 2001 From: Danial Behzadi Date: Wed, 25 Jan 2023 16:03:23 +0000 Subject: [PATCH 49/64] Translated using Weblate (Persian) Currently translated at 100.0% (2597 of 2597 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/fa/ --- library/ui-strings/src/main/res/values-fa/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-fa/strings.xml b/library/ui-strings/src/main/res/values-fa/strings.xml index 1b726a2428..d498f4a51b 100644 --- a/library/ui-strings/src/main/res/values-fa/strings.xml +++ b/library/ui-strings/src/main/res/values-fa/strings.xml @@ -2918,4 +2918,5 @@ از آن‌جا که در حال ضبط پخشی زنده‌اید، نمی‌توانید پیامی صوتی را آغاز کنید. لطفاً برای آغاز ضبط یک پیام صوتی، پخش زنده‌تان را پایان دهید نمی‌توان پخش صوتی را آغاز کرد - + خطای اتّصال - ضبط مکث شد + \ No newline at end of file From 5c4ab205f7bc4ae4c663de15c1175d4dc001d97c Mon Sep 17 00:00:00 2001 From: Glandos Date: Thu, 26 Jan 2023 13:04:46 +0000 Subject: [PATCH 50/64] Translated using Weblate (French) Currently translated at 100.0% (2597 of 2597 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/fr/ --- library/ui-strings/src/main/res/values-fr/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-fr/strings.xml b/library/ui-strings/src/main/res/values-fr/strings.xml index e45211b61a..d62d208e43 100644 --- a/library/ui-strings/src/main/res/values-fr/strings.xml +++ b/library/ui-strings/src/main/res/values-fr/strings.xml @@ -2918,4 +2918,5 @@ Vous ne pouvez pas commencer un message vocal car vous êtes en train d’enregistrer une diffusion en direct. Veuillez terminer cette diffusion pour commencer un message vocal Impossible de démarrer un message vocal - + Erreur de connexion – Enregistrement en pause + \ No newline at end of file From 43dcc405d27876f1e99b1b60cb7f3db1ecee1022 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Thu, 26 Jan 2023 10:17:59 +0000 Subject: [PATCH 51/64] Translated using Weblate (Hungarian) Currently translated at 100.0% (2597 of 2597 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/hu/ --- library/ui-strings/src/main/res/values-hu/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-hu/strings.xml b/library/ui-strings/src/main/res/values-hu/strings.xml index 0aa70cea55..c265b79969 100644 --- a/library/ui-strings/src/main/res/values-hu/strings.xml +++ b/library/ui-strings/src/main/res/values-hu/strings.xml @@ -2918,4 +2918,5 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Nem lehet hang üzenetet indítani élő közvetítés felvétele közben. Az élő közvetítés bejezése szükséges a hang üzenet indításához Hang üzenetet nem lehet elindítani - + Kapcsolódási hiba – Felvétel szüneteltetve + \ No newline at end of file From 882357f6a4dbeecccb44a9e4bde4010836a22107 Mon Sep 17 00:00:00 2001 From: Linerly Date: Wed, 25 Jan 2023 13:00:36 +0000 Subject: [PATCH 52/64] Translated using Weblate (Indonesian) Currently translated at 100.0% (2597 of 2597 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/id/ --- library/ui-strings/src/main/res/values-in/strings.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-in/strings.xml b/library/ui-strings/src/main/res/values-in/strings.xml index 4c524df727..8a05481fd5 100644 --- a/library/ui-strings/src/main/res/values-in/strings.xml +++ b/library/ui-strings/src/main/res/values-in/strings.xml @@ -2858,4 +2858,7 @@ Di masa mendatang proses verifikasi ini akan dimutakhirkan. Tidak ada pemungutan suara aktif %1$d hari terakhir. \nMuat lebih banyak pemungutan suara untuk melihat pemungutan suara untuk hari sebelumnya. - + Kesalahan koneksi - Perekaman dijeda + Anda tidak dapat memulai sebuah pesan suara karena Anda saat ini merekam sebuah siaran langsung. Silakan mengakhiri siaran langsung Anda untuk memulai merekam sebuah pesan suara + Tidak dapat memulai pesan suara + \ No newline at end of file From 356f221caad85cba18dcb29d5bdc0363dfe52ad8 Mon Sep 17 00:00:00 2001 From: Suguru Hirahara Date: Wed, 25 Jan 2023 19:23:07 +0000 Subject: [PATCH 53/64] Translated using Weblate (Japanese) Currently translated at 87.4% (2270 of 2597 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ja/ --- .../src/main/res/values-ja/strings.xml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/library/ui-strings/src/main/res/values-ja/strings.xml b/library/ui-strings/src/main/res/values-ja/strings.xml index d893156f6e..0b987ce683 100644 --- a/library/ui-strings/src/main/res/values-ja/strings.xml +++ b/library/ui-strings/src/main/res/values-ja/strings.xml @@ -198,7 +198,7 @@ メインアドレスとして設定 メインアドレスとしての設定を解除 セッションID - 文字の大きさ + フォントの大きさ とても小さい 小さい 標準 @@ -2391,7 +2391,7 @@ 招待 プッシュ通知 セッション名 - セッションを改名 + セッション名を変更 IPアドレス オペレーティングシステム 形式 @@ -2487,4 +2487,17 @@ %1$dを選択しました - + 有効にすると、このアプリケーションを使用している際にも、他のユーザーにオフラインとして表示されます。 + 最近のチャットをシステムの共有メニューに表示 + システムの既定値を使用 + 手動で設定 + 自動的に設定 + フォントの大きさを選択 + ⚠ 未認証の端末がこのルームにあります。あなたが送信するメッセージを復号化することはできません。 + このルームの未認証のセッションに暗号化されたメッセージを送信しない。 + あなたのホームサーバーはスレッドの一覧表示をまだサポートしていません。 + ここに新しいリクエストと招待が表示されます。 + リッチテキストエディターを試してみる(プレーンテキストモードは近日公開) + タブを使用してElementの表示をシンプルにする + セッションの詳細 + \ No newline at end of file From 684408d6d29864e9dfab8503f74f1ec30eaf1daf Mon Sep 17 00:00:00 2001 From: Didek Date: Fri, 27 Jan 2023 03:06:44 +0000 Subject: [PATCH 54/64] Translated using Weblate (Polish) Currently translated at 93.1% (2418 of 2597 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/pl/ --- .../src/main/res/values-pl/strings.xml | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/library/ui-strings/src/main/res/values-pl/strings.xml b/library/ui-strings/src/main/res/values-pl/strings.xml index 0aad400340..4419187ba5 100644 --- a/library/ui-strings/src/main/res/values-pl/strings.xml +++ b/library/ui-strings/src/main/res/values-pl/strings.xml @@ -345,7 +345,7 @@ Importuj klucze z lokalnego pliku Importuj Szyfruj wiadomości tylko do zaufanych sesji - Nigdy nie wysyłaj szyfrowanych wiadomości do sesji (np urządzeń innych użytkowników) które nie zostały zweryfikowane. + Nigdy nie wysyłaj szyfrowanych wiadomości do niezweryfikowanych sesji (bez zielonej tarczy) z tego urządzenia. Aby sprawdzić czy ta sesja jest zaufana, skontaktuj się z jej właścicielem używając innych form (np. osobiście lub telefonicznie) i zapytaj czy klucz, który widzą w ustawieniach użytkownika dla tego urządzenia pasuje do klucza poniżej: Jeśli klucz pasuje, potwierdź to przyciskiem poniżej. Jeśli nie, to ktoś inny najprawdopodobniej przejmuje lub podszywa się pod tą sesję i powinieneś dodać tę sesję do czarnej listy. W przyszłości proces weryfikacji będzie bardziej skomplikowany. Wyślij naklejkę @@ -1115,7 +1115,7 @@ \nKlucze nie są zaufane Podpis krzyżowy nie jest aktywowany Aktywne Sesje - Pokaż wszystkie Sesje + Pokaż wszystkie sesje Zarządzaj Sesjami Wyloguj z tej sesji Brak dostępnej informacji o kryptografii @@ -1242,7 +1242,7 @@ Zapisz Klucz Bezpieczeństwa Użyj Frazy Bezpieczeństwa Użyj klucza bezpieczeństwa - Zabezpiecza przeciwko utracie dostępu do zaszyfrowanych wiadomości oraz danych poprzez zapisanie zaszyfrowanych kluczy na Twoim serwerze. + Zabezpiecza przed utratą dostępu do zaszyfrowanych wiadomości poprzez zapisanie kluczy szyfrujących na twoim serwerze. Włącz aparat Wyłącz aparat Wyłącz wyciszenie mikrofonu @@ -1493,7 +1493,7 @@ Integracje są zablokowane To zastąpi obecny Klucz bądź Hasło. Wygeneruj nowy klucz bezpieczeństwa albo hasło dla istniejącej kopii zapasowej. - Zabezpiecza przeciwko utracie dostępu do zaszyfrowanych wiadomości oraz danych poprzez zapisanie zaszyfrowanych kluczy na Twoim serwerze. + Zabezpiecza przed utratą dostępu do zaszyfrowanych wiadomości poprzez zapisanie kluczy szyfrujących na twoim serwerze. Powiadomienie zostało kliknięte! Proszę kliknąć na powiadomieniu, Jeżeli nie widzisz powiadomienia, sprawdź ustawienia systemowe. Widzisz powiadomienia! Kliknij na mnie! @@ -2795,4 +2795,36 @@ Rozumiem Zwiń %s pokojów Rozwiń %s pokojów - + Nieaktywne sesje + Ta sesja jest gotowa do bezpiecznego przesyłania wiadomości. + Twoja bieżąca sesja jest gotowa do bezpiecznego przesyłania wiadomości. + Kontakt + Lokalizacja + Aparat + Transmisja głosowa + Rozpocznij transmisję głosową + Ostatnie ankiety + W tym pokoju nie ma aktywnych ankiet + Aktywne ankiety + Niektóre głosy mogą nie zostać policzone z powodu błędów w odszyfrowaniu + Zakończono ankietę. + Błąd połączenia - Nagrywanie wstrzymane + Nie można odtworzyć tej transmisji głosowej. + Jesteś już w trakcie nagrywania transmisji głosowej. Proszę zakończyć bieżącą transmisję, aby rozpocząć nową. + Ktoś inny nagrywa już transmisję głosową. Aby rozpocząć nową transmisję, należy poczekać na jej zakończenie. + Nie masz wymaganych uprawnień do rozpoczęcia transmisji głosowej w tym pokoju. Skontaktuj się z administratorem pokoju, aby przyznał ci uprawnienia. + Nie można rozpocząć nowej transmisji głosowej + Buforowanie… + Nie można rozpocząć wiadomości głosowej + Masz niezweryfikowane sesje + Autentyczność tej zaszyfrowanej wiadomości nie może być zagwarantowana na tym urządzeniu. + Historia ankiet + Dodaje (╯°□°)╯︵ ┻━┻ do wiadomości tekstowej + Skanuj kod QR + %1$s zakończył(a) transmisję głosową. + Zarządaj od systemu Android aby klawiatura nie zapisywała żadnych danych takich jak historia pisania lub słownik. Pamiętaj, nie niektóre klawiatury mogą nie zastosować się do tego ustawienia. + Klawiatura incognito + Witaj w ${app_name}, +\n%s. + Wszechstronna, bezpieczna aplikacja do czatowania dla zespołów, przyjaciół i organizacji. Utwórz czat lub dołącz do istniejącego pokoju, aby rozpocząć. + \ No newline at end of file From b2eb65cd0be9890da2719d5ed898efdce1884b75 Mon Sep 17 00:00:00 2001 From: DarkCoder15 Date: Thu, 26 Jan 2023 15:24:02 +0000 Subject: [PATCH 55/64] Translated using Weblate (Russian) Currently translated at 100.0% (2597 of 2597 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ru/ --- .../src/main/res/values-ru/strings.xml | 72 ++++++++++++++++--- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/library/ui-strings/src/main/res/values-ru/strings.xml b/library/ui-strings/src/main/res/values-ru/strings.xml index 1255776c1f..5f2d383460 100644 --- a/library/ui-strings/src/main/res/values-ru/strings.xml +++ b/library/ui-strings/src/main/res/values-ru/strings.xml @@ -65,7 +65,7 @@ %1$s отклонил приглашение. Причина: %2$s %1$s выгнали %2$s. Причина: %3$s %1$s разблокировано %2$s. Причина: %3$s - %1$s забанен %2$s. Причина: %3$s + %1$s забанил %2$s. Причина: %3$s %1$s принял приглашение для %2$s. Причина: %3$s %1$s отозвал приглашение %2$s. Причина: %3$s %1$s создал(а) комнату @@ -1420,7 +1420,7 @@ Посылает сообщение, окрашенное в цвет радуги Посылает данную эмоцию, окрашенную в цвет радуги Редактор сообщений - Включаем сквозное шифрование… + Включить сквозное шифрование… Включить шифрование\? После включения шифрование для комнаты нельзя отключить. Сообщения отправленные в зашифрованной комнате не будут видны серверу, только участникам комнаты. Включение шифрования может помешать правильной работе многих ботов и мостов. Включить шифрование @@ -2433,7 +2433,7 @@ Не удалось загрузить карту Карта Примечание: приложение будет перезапущено - Обсуждения сообщений + Включить обсуждения сообщений Подключиться к серверу Хотите присоединиться к существующему серверу\? Пропустить вопрос @@ -2540,7 +2540,7 @@ Домашний сервер не принимает имя пользователя, состоящее только из цифр. Пропустить этот шаг Сохранить и продолжить - Ваши предпочтения были сохранены. + Зайдите в настройки чтобы изменить Ваш профиль Выглядит хорошо! ${app_name} также отлично подходит для работы. Ему доверяют самые надёжные организации в мире. Резервная копия имеет действительную подпись для данного пользователя. @@ -2791,7 +2791,7 @@ Рассмотрите возможность выхода из старых сеансов (%1$d дней или дольше), которые вы более не используете. Голосовая трансляция - Голосовые трансляции + Включить голосовые трансляции Записывает название клиента, версию и URL-адрес для более лёгкого распознавания сеансов в менеджере сеансов. Записывать информацию о клиенте Галерея @@ -2824,9 +2824,9 @@ Развернуть дочерние элементы %s Выбрано %1$d - Выбрано %1$d - Выбрано %1$d - Выбрано %1$d + Выбраны %1$d + Выбраны %1$d + Выбраны %1$d Войти в полноэкранный режим Применить форматирование подчёркиванием @@ -2970,4 +2970,58 @@ Этот сеанс не поддерживает шифрование и поэтому не может быть заверен. %1$s завершил(а) голосовую трансляцию. Вы завершили голосовую трансляцию. - + + Нет активных опросов за %1$d день. +\nЗагрузите больше чтобы показать опросы за прошедшие дни. + Нет активных опросов за %1$d дней. +\nЗагрузите больше чтобы показать опросы за прошедшие дни. + Нет активных опросов за %1$d дней. +\nЗагрузите больше чтобы показать опросы за прошедшие дни. + Нет активных опросов за %1$d дней. +\nЗагрузите больше чтобы показать опросы за прошедшие дни. + + + Нет завершённых опросов за день %1$d. +\nЗагрузите больше чтобы показать опросы за предыдущие дни. + Нет завершённых опросов за %1$d дней +\nЗагрузите больше чтобы показать опросы за предыдущие дни. + Нет завершённых опросов за %1$d дней +\nЗагрузите больше чтобы показать опросы за предыдущие дни. + Нет завершённых опросов за %1$d дней +\nЗагрузите больше чтобы показать опросы за предыдущие дни. + + Токен доступа даёт полный доступ к аккаунту. Не делитесь им ни с кем. + Токен доступа + Завершённый опрос + Опрос + завершённый опрос. + Изменить ссылку + Создать ссылку + Ссылка + Текст + Список + Пронумерованный список + Ссылка + Ошибка считывания опросов. + Загрузить больше опросов + Показываем опросы + Нет завершённых опросов + Завершённые опросы + Нет активных опросов + Активные опросы + Из-за ошибок расшифровки, некоторые голоса могут быть не засчитаны + Опрос завершён. + Вы уверены что хотите завершить голосовую трансляцию\? Это завершит трансляцию и полная запись будет доступна в чате. + Завершить голосовую трансляцию\? + Ошибка подключения - Запись приостановлена + Невозможно прослушать голосовую трансляцию. + Голосовая трансляция + Вы не можете записать голосовое сообщение, потому-что Вы записываете голосовую трансляцию. Завершите голосовую трансляцию, чтобы записать голосовое сообщение + Не удалось записать голосовое сообщение + Убедиться что Ваш аккаунт в безопасности + Получить последнюю сборку (у вас могут быть проблемы со входом) + История опроса + Голосовая трансляция начата + Ваш домашний сервер не поддерживает список обсуждений. + Остановить + \ No newline at end of file From 2b5fb3bfbfde21f5fba11fd600d0547eadbd386c Mon Sep 17 00:00:00 2001 From: Jozef Gaal Date: Wed, 25 Jan 2023 19:01:47 +0000 Subject: [PATCH 56/64] Translated using Weblate (Slovak) Currently translated at 100.0% (2597 of 2597 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/sk/ --- library/ui-strings/src/main/res/values-sk/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-sk/strings.xml b/library/ui-strings/src/main/res/values-sk/strings.xml index ed3f47f9d3..c9e92d323b 100644 --- a/library/ui-strings/src/main/res/values-sk/strings.xml +++ b/library/ui-strings/src/main/res/values-sk/strings.xml @@ -2978,4 +2978,5 @@ Nemôžete spustiť hlasovú správu, pretože práve nahrávate živé vysielanie. Ukončite prosím živé vysielanie, aby ste mohli začať nahrávať hlasovú správu Nemožno spustiť hlasovú správu - + Chyba pripojenia - nahrávanie pozastavené + \ No newline at end of file From b01fd17413cf1bec58acb887c9918136478aeb30 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Wed, 25 Jan 2023 17:38:17 +0000 Subject: [PATCH 57/64] Translated using Weblate (Ukrainian) Currently translated at 100.0% (2597 of 2597 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/uk/ --- library/ui-strings/src/main/res/values-uk/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-uk/strings.xml b/library/ui-strings/src/main/res/values-uk/strings.xml index 6294526be2..0f6027903f 100644 --- a/library/ui-strings/src/main/res/values-uk/strings.xml +++ b/library/ui-strings/src/main/res/values-uk/strings.xml @@ -3038,4 +3038,5 @@ Показ опитувань Ви не можете розпочати запис голосового повідомлення, оскільки ви записуєте трансляцію наживо. Будь ласка, заверште її, щоб розпочати запис голосового повідомлення Не вдалося розпочати запис голосового повідомлення - + Помилка з\'єднання - Запис призупинено + \ No newline at end of file From d83efde9f03abfe7cefc51ff4c6f0c402046a8b3 Mon Sep 17 00:00:00 2001 From: DarkCoder15 Date: Thu, 26 Jan 2023 15:33:23 +0000 Subject: [PATCH 58/64] Translated using Weblate (Russian) Currently translated at 100.0% (90 of 90 strings) Translation: Element Android/Element Android Store Translate-URL: https://translate.element.io/projects/element-android/element-store/ru/ --- fastlane/metadata/android/ru-RU/changelogs/40105110.txt | 2 ++ fastlane/metadata/android/ru-RU/changelogs/40105140.txt | 2 ++ fastlane/metadata/android/ru-RU/changelogs/40105160.txt | 2 ++ fastlane/metadata/android/ru-RU/changelogs/40105180.txt | 2 ++ fastlane/metadata/android/ru-RU/changelogs/40105200.txt | 2 ++ 5 files changed, 10 insertions(+) create mode 100644 fastlane/metadata/android/ru-RU/changelogs/40105110.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/40105140.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/40105160.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/40105180.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/40105200.txt diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105110.txt b/fastlane/metadata/android/ru-RU/changelogs/40105110.txt new file mode 100644 index 0000000000..3de0ce886e --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40105110.txt @@ -0,0 +1,2 @@ +Главные изменения в этой версии: Новый полноэкранный режим в улучшенном редакторе текста и исправления багов. +Полный список: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105140.txt b/fastlane/metadata/android/ru-RU/changelogs/40105140.txt new file mode 100644 index 0000000000..9700aef76f --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40105140.txt @@ -0,0 +1,2 @@ +Главные изменения в этой версии: Обсуждения включены по умолчанию. +Полный список: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105160.txt b/fastlane/metadata/android/ru-RU/changelogs/40105160.txt new file mode 100644 index 0000000000..9700aef76f --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40105160.txt @@ -0,0 +1,2 @@ +Главные изменения в этой версии: Обсуждения включены по умолчанию. +Полный список: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105180.txt b/fastlane/metadata/android/ru-RU/changelogs/40105180.txt new file mode 100644 index 0000000000..9700aef76f --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40105180.txt @@ -0,0 +1,2 @@ +Главные изменения в этой версии: Обсуждения включены по умолчанию. +Полный список: https://github.com/vector-im/element-android/releases diff --git a/fastlane/metadata/android/ru-RU/changelogs/40105200.txt b/fastlane/metadata/android/ru-RU/changelogs/40105200.txt new file mode 100644 index 0000000000..9ee0bb9656 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/40105200.txt @@ -0,0 +1,2 @@ +Главные изменения в этой версии: Устранения багов! +Полный список: https://github.com/vector-im/element-android/releases From 366ce8665d43ab264896aab8abe60af54162c355 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Jan 2023 16:40:20 +0100 Subject: [PATCH 59/64] Do not show unknown data. --- .../im/vector/app/features/home/HomeDetailViewState.kt | 4 ++-- .../features/home/room/detail/RoomDetailViewState.kt | 4 ++-- .../vector/app/features/sync/widget/SyncStateView.kt | 10 ++++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt index 7fbd5b2bf6..dcf4d87894 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailViewState.kt @@ -38,8 +38,8 @@ data class HomeDetailViewState( val notificationCountRooms: Int = 0, val notificationHighlightRooms: Boolean = false, val hasUnreadMessages: Boolean = false, - val syncState: SyncState = SyncState.Idle, - val incrementalSyncRequestState: SyncRequestState.IncrementalSyncRequestState = SyncRequestState.IncrementalSyncIdle, + val syncState: SyncState? = null, + val incrementalSyncRequestState: SyncRequestState.IncrementalSyncRequestState? = null, val pushCounter: Int = 0, val pstnSupportFlag: Boolean = false, val forceDialPadTab: Boolean = false diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index 897594ffad..f4919a5906 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -60,8 +60,8 @@ data class RoomDetailViewState( val formattedTypingUsers: String? = null, val tombstoneEvent: Event? = null, val joinUpgradedRoomAsync: Async = Uninitialized, - val syncState: SyncState = SyncState.Idle, - val incrementalSyncRequestState: SyncRequestState.IncrementalSyncRequestState = SyncRequestState.IncrementalSyncIdle, + val syncState: SyncState? = null, + val incrementalSyncRequestState: SyncRequestState.IncrementalSyncRequestState? = null, val pushCounter: Int = 0, val highlightedEventId: String? = null, val unreadState: UnreadState = UnreadState.Unknown, diff --git a/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt b/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt index a71f445859..a40b70b8a6 100755 --- a/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt +++ b/vector/src/main/java/im/vector/app/features/sync/widget/SyncStateView.kt @@ -40,8 +40,8 @@ class SyncStateView @JvmOverloads constructor(context: Context, attrs: Attribute @SuppressLint("SetTextI18n") fun render( - newState: SyncState, - incrementalSyncRequestState: SyncRequestState.IncrementalSyncRequestState, + newState: SyncState?, + incrementalSyncRequestState: SyncRequestState.IncrementalSyncRequestState?, pushCounter: Int, showDebugInfo: Boolean ) { @@ -64,8 +64,9 @@ class SyncStateView @JvmOverloads constructor(context: Context, attrs: Attribute } } - private fun SyncState.toHumanReadable(): String { + private fun SyncState?.toHumanReadable(): String { return when (this) { + null -> "Unknown" SyncState.Idle -> "Idle" SyncState.InvalidToken -> "InvalidToken" SyncState.Killed -> "Killed" @@ -76,8 +77,9 @@ class SyncStateView @JvmOverloads constructor(context: Context, attrs: Attribute } } - private fun SyncRequestState.IncrementalSyncRequestState.toHumanReadable(): String { + private fun SyncRequestState.IncrementalSyncRequestState?.toHumanReadable(): String { return when (this) { + null -> "Unknown" SyncRequestState.IncrementalSyncIdle -> "Idle" is SyncRequestState.IncrementalSyncParsing -> "Parsing ${this.rooms} room(s) ${this.toDevice} toDevice(s)" SyncRequestState.IncrementalSyncError -> "Error" From 5a62e31c86ac55256d7915606b11d25cf830e12f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Jan 2023 16:44:35 +0100 Subject: [PATCH 60/64] Ensure sync thread is started when recovering from crash. --- .../session/EnsureSessionSyncingUseCase.kt | 38 +++++++++++++++++++ .../features/home/HomeActivityViewModel.kt | 4 ++ 2 files changed, 42 insertions(+) create mode 100644 vector/src/main/java/im/vector/app/core/session/EnsureSessionSyncingUseCase.kt diff --git a/vector/src/main/java/im/vector/app/core/session/EnsureSessionSyncingUseCase.kt b/vector/src/main/java/im/vector/app/core/session/EnsureSessionSyncingUseCase.kt new file mode 100644 index 0000000000..c53795d18d --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/session/EnsureSessionSyncingUseCase.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 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.core.session + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.extensions.startSyncing +import org.matrix.android.sdk.api.session.sync.SyncState +import timber.log.Timber +import javax.inject.Inject + +class EnsureSessionSyncingUseCase @Inject constructor( + @ApplicationContext private val context: Context, + private val activeSessionHolder: ActiveSessionHolder, +) { + fun execute() { + val session = activeSessionHolder.getSafeActiveSession() ?: return + if (session.syncService().getSyncState() == SyncState.Idle) { + Timber.w("EnsureSessionSyncingUseCase: start syncing") + session.startSyncing(context) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 8f16121a30..f961cc5da6 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -30,6 +30,7 @@ import im.vector.app.core.pushers.EnsureFcmTokenIsRetrievedUseCase import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.RegisterUnifiedPushUseCase import im.vector.app.core.pushers.UnregisterUnifiedPushUseCase +import im.vector.app.core.session.EnsureSessionSyncingUseCase import im.vector.app.features.analytics.AnalyticsConfig import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.extensions.toAnalyticsType @@ -95,6 +96,7 @@ class HomeActivityViewModel @AssistedInject constructor( private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, private val ensureFcmTokenIsRetrievedUseCase: EnsureFcmTokenIsRetrievedUseCase, + private val ensureSessionSyncingUseCase: EnsureSessionSyncingUseCase, ) : VectorViewModel(initialState) { @AssistedFactory @@ -118,6 +120,8 @@ class HomeActivityViewModel @AssistedInject constructor( private fun initialize() { if (isInitialized) return isInitialized = true + // Ensure Session is syncing + ensureSessionSyncingUseCase.execute() registerUnifiedPushIfNeeded() cleanupFiles() observeInitialSync() From d6712b7c93d11f50f001c663ab017706b0afec8c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Jan 2023 17:01:52 +0100 Subject: [PATCH 61/64] Fix layout issue with `Messages failed to send` banner. --- .../src/main/res/layout/fragment_timeline.xml | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/vector/src/main/res/layout/fragment_timeline.xml b/vector/src/main/res/layout/fragment_timeline.xml index 6e83dbe8fd..a022ad2744 100644 --- a/vector/src/main/res/layout/fragment_timeline.xml +++ b/vector/src/main/res/layout/fragment_timeline.xml @@ -17,7 +17,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="48dp" - android:visibility="gone" /> + android:visibility="gone" + tools:visibility="visible" /> + android:layout_height="match_parent"> + app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView" + tools:visibility="visible" /> + + - - + android:translationZ="10dp" + android:visibility="visible" /> From 0c89245392673ddcf3cf36dd32735798061a2410 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Jan 2023 18:13:27 +0100 Subject: [PATCH 62/64] Fix test compilation --- .../java/im/vector/app/features/command/CommandParserTest.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vector/src/test/java/im/vector/app/features/command/CommandParserTest.kt b/vector/src/test/java/im/vector/app/features/command/CommandParserTest.kt index f502db85ca..ef6a99aa49 100644 --- a/vector/src/test/java/im/vector/app/features/command/CommandParserTest.kt +++ b/vector/src/test/java/im/vector/app/features/command/CommandParserTest.kt @@ -16,12 +16,15 @@ package im.vector.app.features.command +import im.vector.app.test.fakes.FakeVectorPreferences import org.amshove.kluent.shouldBeEqualTo import org.junit.Test private const val A_SPACE_ID = "!my-space-id" class CommandParserTest { + private val fakeVectorPreferences = FakeVectorPreferences() + @Test fun parseSlashCommandEmpty() { test("/", ParsedCommand.ErrorEmptySlashCommand) @@ -70,7 +73,7 @@ class CommandParserTest { } private fun test(message: String, expectedResult: ParsedCommand) { - val commandParser = CommandParser() + val commandParser = CommandParser(fakeVectorPreferences.instance) val result = commandParser.parseSlashCommand(message, null, false) result shouldBeEqualTo expectedResult } From 3104f62988d0224254e4386b8f87c6ed2a431784 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Jan 2023 23:03:50 +0000 Subject: [PATCH 63/64] Bump sentry-android from 6.12.1 to 6.13.0 Bumps [sentry-android](https://github.com/getsentry/sentry-java) from 6.12.1 to 6.13.0. - [Release notes](https://github.com/getsentry/sentry-java/releases) - [Changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-java/compare/6.12.1...6.13.0) --- updated-dependencies: - dependency-name: io.sentry:sentry-android dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 5d7286ab1a..bab9229b3b 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -27,7 +27,7 @@ def jjwt = "0.11.5" // Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert // the whole commit which set version 0.16.0-SNAPSHOT def vanniktechEmoji = "0.16.0-SNAPSHOT" -def sentry = "6.12.1" +def sentry = "6.13.0" // Use 1.6.0 alpha to fix issue with test def fragment = "1.6.0-alpha04" // Testing From 934e9178b4b34cce44ec4aa966165d1e6f72651e Mon Sep 17 00:00:00 2001 From: DarkCoder15 Date: Sat, 28 Jan 2023 09:09:26 +0000 Subject: [PATCH 64/64] Translated using Weblate (Russian) Currently translated at 100.0% (2597 of 2597 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/ru/ --- library/ui-strings/src/main/res/values-ru/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-ru/strings.xml b/library/ui-strings/src/main/res/values-ru/strings.xml index 5f2d383460..5938200c1e 100644 --- a/library/ui-strings/src/main/res/values-ru/strings.xml +++ b/library/ui-strings/src/main/res/values-ru/strings.xml @@ -6,7 +6,7 @@ %1$s вошёл(шла) в комнату %1$s покинул(а) комнату %1$s отклонил(а) приглашение - %1$s выгнан %2$s + %1$s выгнал %2$s %1$s разблокировал(а) %2$s %1$s заблокировал(а) %2$s %1$s отозвал(а) приглашение %2$s