From cf3abd6562f37124e4996cdccbc478dea8bfcd33 Mon Sep 17 00:00:00 2001 From: Nikita Fedrunov <66663241+fedrunov@users.noreply.github.com> Date: Wed, 14 Dec 2022 18:56:16 +0100 Subject: [PATCH] thread list loading (#7766) --- changelog.d/5819.misc | 1 + .../org/matrix/android/sdk/flow/FlowRoom.kt | 8 -- .../room/threads/FetchThreadsResult.kt | 23 ++++ .../api/session/room/threads/ThreadFilter.kt | 26 +++++ .../room/threads/ThreadLivePageResult.kt | 27 +++++ .../session/room/threads/ThreadsService.kt | 16 +-- .../database/RealmSessionStoreMigration.kt | 4 +- .../database/helper/ThreadSummaryHelper.kt | 24 ++-- .../database/migration/MigrateSessionTo046.kt | 32 ++++++ .../database/model/SessionRealmModule.kt | 2 + .../model/threads/ThreadListPageEntity.kt | 28 +++++ .../model/threads/ThreadSummaryEntity.kt | 3 + .../query/ThreadSummaryPageEntityQueries.kt | 31 +++++ .../sdk/internal/session/room/RoomAPI.kt | 9 ++ .../threads/FetchThreadSummariesTask.kt | 71 ++++++------ .../threads/ThreadSummariesResponse.kt | 27 +++++ .../room/threads/DefaultThreadsService.kt | 83 ++++++++++---- .../list/viewmodel/ThreadListController.kt | 66 +---------- .../viewmodel/ThreadListPagedController.kt | 84 ++++++++++++++ .../list/viewmodel/ThreadListViewModel.kt | 107 ++++++++++++++---- .../list/viewmodel/ThreadListViewState.kt | 2 - .../threads/list/views/ThreadListFragment.kt | 27 ++++- 22 files changed, 526 insertions(+), 175 deletions(-) create mode 100644 changelog.d/5819.misc create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadFilter.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadLivePageResult.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo046.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadListPageEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryPageEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/ThreadSummariesResponse.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListPagedController.kt diff --git a/changelog.d/5819.misc b/changelog.d/5819.misc new file mode 100644 index 0000000000..5f2d05dc3c --- /dev/null +++ b/changelog.d/5819.misc @@ -0,0 +1 @@ +[Threads] - added API to fetch threads list from the server instead of building it locally from events diff --git a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt index 7ad342b22f..94f09e0bf5 100644 --- a/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt +++ b/matrix-sdk-android-flow/src/main/java/org/matrix/android/sdk/flow/FlowRoom.kt @@ -31,7 +31,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.session.room.send.UserDraft -import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional @@ -119,13 +118,6 @@ class FlowRoom(private val room: Room) { return room.roomPushRuleService().getLiveRoomNotificationState().asFlow() } - fun liveThreadSummaries(): Flow> { - return room.threadsService().getAllThreadSummariesLive().asFlow() - .startWith(room.coroutineDispatchers.io) { - room.threadsService().getAllThreadSummaries() - } - } - fun liveThreadList(): Flow> { return room.threadsLocalService().getAllThreadsLive().asFlow() .startWith(room.coroutineDispatchers.io) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt new file mode 100644 index 0000000000..5d4d67a65e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2022 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.threads + +sealed class FetchThreadsResult { + data class ShouldFetchMore(val nextBatch: String) : FetchThreadsResult() + object ReachedEnd : FetchThreadsResult() + object Failed : FetchThreadsResult() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadFilter.kt new file mode 100644 index 0000000000..3f3576728f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadFilter.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2022 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.threads + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +enum class ThreadFilter { + @Json(name = "all") ALL, + @Json(name = "participated") PARTICIPATED, +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadLivePageResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadLivePageResult.kt new file mode 100644 index 0000000000..7693dc6fde --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadLivePageResult.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 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.threads + +import androidx.lifecycle.LiveData +import androidx.paging.PagedList +import org.matrix.android.sdk.api.session.room.ResultBoundaries +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary + +data class ThreadLivePageResult( + val livePagedList: LiveData>, + val liveBoundaries: LiveData +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt index 9587be68f1..bb6f6b51d3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt @@ -16,7 +16,7 @@ package org.matrix.android.sdk.api.session.room.threads -import androidx.lifecycle.LiveData +import androidx.paging.PagedList import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary /** @@ -27,15 +27,14 @@ import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary */ interface ThreadsService { - /** - * Returns a [LiveData] list of all the [ThreadSummary] that exists at the room level. - */ - fun getAllThreadSummariesLive(): LiveData> + suspend fun getPagedThreadsList(userParticipating: Boolean, pagedListConfig: PagedList.Config): ThreadLivePageResult + + suspend fun fetchThreadList(nextBatchId: String?, limit: Int, filter: ThreadFilter = ThreadFilter.ALL): FetchThreadsResult /** * Returns a list of all the [ThreadSummary] that exists at the room level. */ - fun getAllThreadSummaries(): List + suspend fun getAllThreadSummaries(): List /** * Enhance the provided ThreadSummary[List] by adding the latest @@ -51,9 +50,4 @@ interface ThreadsService { * @param limit defines the number of max results the api will respond with */ suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int) - - /** - * Fetch all thread summaries for the current room using the enhanced /messages api. - */ - suspend fun fetchThreadSummaries() } 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 2fb87ca874..5295abffe3 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 @@ -62,6 +62,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo042 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo043 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo044 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo045 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo046 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -70,7 +71,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 45L, + schemaVersion = 46L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -125,5 +126,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 43) MigrateSessionTo043(realm).perform() if (oldVersion < 44) MigrateSessionTo044(realm).perform() if (oldVersion < 45) MigrateSessionTo045(realm).perform() + if (oldVersion < 46) MigrateSessionTo046(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt index 0ac8dc7902..908c710df4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt @@ -37,9 +37,11 @@ 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.model.RoomEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.database.query.get import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.where @@ -113,16 +115,16 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate( userId: String, cryptoService: CryptoService? = null, currentTimeMillis: Long, -) { +): ThreadSummaryEntity? { when (threadSummaryType) { ThreadSummaryUpdateType.REPLACE -> { - rootThreadEvent?.eventId ?: return - rootThreadEvent.senderId ?: return + rootThreadEvent?.eventId ?: return null + rootThreadEvent.senderId ?: return null - val numberOfThreads = rootThreadEvent.unsignedData?.relations?.latestThread?.count ?: return + val numberOfThreads = rootThreadEvent.unsignedData?.relations?.latestThread?.count ?: return null // Something is wrong with the server return - if (numberOfThreads <= 0) return + if (numberOfThreads <= 0) return null val threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEvent.eventId).also { Timber.i("###THREADS ThreadSummaryHelper REPLACE eventId:${it.rootThreadEventId} ") @@ -153,12 +155,13 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate( ) roomEntity.addIfNecessary(threadSummary) + return threadSummary } ThreadSummaryUpdateType.ADD -> { - val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return + val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return null Timber.i("###THREADS ThreadSummaryHelper ADD for root eventId:$rootThreadEventId") - val threadSummary = ThreadSummaryEntity.getOrNull(realm, roomId, rootThreadEventId) + var threadSummary = ThreadSummaryEntity.getOrNull(realm, roomId, rootThreadEventId) if (threadSummary != null) { // ThreadSummary exists so lets add the latest event Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId exists, lets update latest thread event.") @@ -172,7 +175,7 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate( Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId do not exists, lets try to create one") threadEventEntity.findRootThreadEvent()?.let { rootThreadEventEntity -> // Root thread event entity exists so lets create a new record - ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEventEntity.eventId).let { + threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEventEntity.eventId).also { it.updateThreadSummary( rootThreadEventEntity = rootThreadEventEntity, numberOfThreads = 1, @@ -183,7 +186,12 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate( roomEntity.addIfNecessary(it) } } + + threadSummary?.let { + ThreadListPageEntity.get(realm, roomId)?.threadSummaries?.add(it) + } } + return threadSummary } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo046.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo046.kt new file mode 100644 index 0000000000..4b1d2059a4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo046.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 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.threads.ThreadListPageEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo046(realm: DynamicRealm) : RealmMigrator(realm, 46) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.create("ThreadListPageEntity") + .addField(ThreadListPageEntityFields.ROOM_ID, String::class.java) + .addPrimaryKey(ThreadListPageEntityFields.ROOM_ID) + .setRequired(ThreadListPageEntityFields.ROOM_ID, true) + .addRealmListField(ThreadListPageEntityFields.THREAD_SUMMARIES.`$`, realm.schema.get("ThreadSummaryEntity")!!) + } +} 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 93ff67a911..0ab30657ed 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 @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.database.model import io.realm.annotations.RealmModule import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity /** @@ -72,6 +73,7 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit UserPresenceEntity::class, ThreadSummaryEntity::class, SyncFilterParamsEntity::class, + ThreadListPageEntity::class ] ) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadListPageEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadListPageEntity.kt new file mode 100644 index 0000000000..1d64c64ddf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadListPageEntity.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2022 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.threads + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class ThreadListPageEntity( + @PrimaryKey var roomId: String = "", + var threadSummaries: RealmList = RealmList() +) : RealmObject() { + companion object +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt index 45f9e3aa20..487be3747a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt @@ -40,5 +40,8 @@ internal open class ThreadSummaryEntity( @LinkingObjects("threadSummaries") val room: RealmResults? = null + @LinkingObjects("threadSummaries") + val page: RealmResults? = null + companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryPageEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryPageEntityQueries.kt new file mode 100644 index 0000000000..9525e55787 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryPageEntityQueries.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2022 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.threads.ThreadListPageEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntityFields + +internal fun ThreadListPageEntity.Companion.get(realm: Realm, roomId: String): ThreadListPageEntity? { + return realm.where().equalTo(ThreadListPageEntityFields.ROOM_ID, roomId).findFirst() +} + +internal fun ThreadListPageEntity.Companion.getOrCreate(realm: Realm, roomId: String): ThreadListPageEntity { + return get(realm, roomId) ?: realm.createObject(roomId) +} 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 4e55b2c40a..ddb7d6a8e6 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 @@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.session.room.membership.joining.InviteBod import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody import org.matrix.android.sdk.internal.session.room.read.ReadBody import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse +import org.matrix.android.sdk.internal.session.room.relation.threads.ThreadSummariesResponse import org.matrix.android.sdk.internal.session.room.reporting.ReportContentBody import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.session.room.tags.TagBody @@ -464,4 +465,12 @@ internal interface RoomAPI { @Path("roomIdOrAlias") roomidOrAlias: String, @Query("via") viaServers: List? ): RoomStrippedState + + @GET(NetworkConstants.URI_API_PREFIX_PATH_V1 + "rooms/{roomId}/threads") + suspend fun getThreadsList( + @Path("roomId") roomId: String, + @Query("include") include: String? = "all", + @Query("from") from: String? = null, + @Query("limit") limit: Int? = null + ): ThreadSummariesResponse } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt index 254dee4295..848b9698ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt @@ -16,37 +16,38 @@ package org.matrix.android.sdk.internal.session.room.relation.threads import com.zhuinden.monarchy.Monarchy +import io.realm.RealmList import org.matrix.android.sdk.api.session.room.model.RoomMemberContent +import org.matrix.android.sdk.api.session.room.threads.FetchThreadsResult +import org.matrix.android.sdk.api.session.room.threads.ThreadFilter import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.database.helper.createOrUpdate import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity +import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest -import org.matrix.android.sdk.internal.session.filter.FilterFactory import org.matrix.android.sdk.internal.session.room.RoomAPI -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 org.matrix.android.sdk.internal.util.time.Clock -import timber.log.Timber import javax.inject.Inject /*** * This class is responsible to Fetch all the thread in the current room, * To fetch all threads in a room, the /messages API is used with newly added filtering options. */ -internal interface FetchThreadSummariesTask : Task { +internal interface FetchThreadSummariesTask : Task { data class Params( val roomId: String, - val from: String = "", - val limit: Int = 500, - val isUserParticipating: Boolean = true + val from: String? = null, + val limit: Int = 5, + val filter: ThreadFilter? = null, ) } @@ -59,39 +60,43 @@ internal class DefaultFetchThreadSummariesTask @Inject constructor( private val clock: Clock, ) : FetchThreadSummariesTask { - override suspend fun execute(params: FetchThreadSummariesTask.Params): Result { - val filter = FilterFactory.createThreadsFilter( - numberOfEvents = params.limit, - userId = if (params.isUserParticipating) userId else null - ).toJSONString() - - val response = executeRequest( - globalErrorReceiver, - canRetry = true - ) { - roomAPI.getRoomMessagesFrom(params.roomId, params.from, PaginationDirection.BACKWARDS.value, params.limit, filter) + override suspend fun execute(params: FetchThreadSummariesTask.Params): FetchThreadsResult { + val response = executeRequest(globalErrorReceiver) { + roomAPI.getThreadsList( + roomId = params.roomId, + include = params.filter?.toString()?.lowercase(), + from = params.from, + limit = params.limit + ) } - Timber.i("###THREADS DefaultFetchThreadSummariesTask Fetched size:${response.events.size} nextBatch:${response.end} ") + handleResponse(response, params) - return handleResponse(response, params) + return when { + response.nextBatch != null -> FetchThreadsResult.ShouldFetchMore(response.nextBatch) + else -> FetchThreadsResult.ReachedEnd + } } private suspend fun handleResponse( - response: PaginationResponse, + response: ThreadSummariesResponse, params: FetchThreadSummariesTask.Params - ): Result { - val rootThreadList = response.events + ) { + val rootThreadList = response.chunk + + val threadSummaries = RealmList() + monarchy.awaitTransaction { realm -> val roomEntity = RoomEntity.where(realm, roomId = params.roomId).findFirst() ?: return@awaitTransaction val roomMemberContentsByUser = HashMap() + for (rootThreadEvent in rootThreadList) { if (rootThreadEvent.eventId == null || rootThreadEvent.senderId == null || rootThreadEvent.type == null) { continue } - ThreadSummaryEntity.createOrUpdate( + val threadSummary = ThreadSummaryEntity.createOrUpdate( threadSummaryType = ThreadSummaryUpdateType.REPLACE, realm = realm, roomId = params.roomId, @@ -102,14 +107,16 @@ internal class DefaultFetchThreadSummariesTask @Inject constructor( cryptoService = cryptoService, currentTimeMillis = clock.epochMillis(), ) + + threadSummaries.add(threadSummary) + } + + val page = ThreadListPageEntity.getOrCreate(realm, params.roomId) + threadSummaries.forEach { + if (!page.threadSummaries.contains(it)) { + page.threadSummaries.add(it) + } } } - return Result.SUCCESS - } - - enum class Result { - SHOULD_FETCH_MORE, - REACHED_END, - SUCCESS } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/ThreadSummariesResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/ThreadSummariesResponse.kt new file mode 100644 index 0000000000..d37a058ef6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/ThreadSummariesResponse.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2022 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.relation.threads + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.session.events.model.Event + +@JsonClass(generateAdapter = true) +internal data class ThreadSummariesResponse( + @Json(name = "chunk") val chunk: List, + @Json(name = "next_batch") val nextBatch: String?, + @Json(name = "prev_batch") val prevBatch: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt index 6c6d6368d1..63756811f9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt @@ -16,32 +16,39 @@ package org.matrix.android.sdk.internal.session.room.threads -import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList import com.zhuinden.monarchy.Monarchy import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.realm.Realm +import io.realm.Sort +import io.realm.kotlin.where +import org.matrix.android.sdk.api.session.room.ResultBoundaries +import org.matrix.android.sdk.api.session.room.threads.FetchThreadsResult +import org.matrix.android.sdk.api.session.room.threads.ThreadFilter +import org.matrix.android.sdk.api.session.room.threads.ThreadLivePageResult import org.matrix.android.sdk.api.session.room.threads.ThreadsService import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.internal.database.helper.enhanceWithEditions import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId import org.matrix.android.sdk.internal.database.mapper.ThreadSummaryMapper -import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.model.threads.ThreadListPageEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity +import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask +import org.matrix.android.sdk.internal.util.awaitTransaction internal class DefaultThreadsService @AssistedInject constructor( @Assisted private val roomId: String, - @UserId private val userId: String, private val fetchThreadTimelineTask: FetchThreadTimelineTask, - private val fetchThreadSummariesTask: FetchThreadSummariesTask, @SessionDatabase private val monarchy: Monarchy, - private val timelineEventMapper: TimelineEventMapper, - private val threadSummaryMapper: ThreadSummaryMapper + private val threadSummaryMapper: ThreadSummaryMapper, + private val fetchThreadSummariesTask: FetchThreadSummariesTask, ) : ThreadsService { @AssistedFactory @@ -49,16 +56,58 @@ internal class DefaultThreadsService @AssistedInject constructor( fun create(roomId: String): DefaultThreadsService } - override fun getAllThreadSummariesLive(): LiveData> { - return monarchy.findAllMappedWithChanges( - { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) }, - { - threadSummaryMapper.map(it) + override suspend fun getPagedThreadsList(userParticipating: Boolean, pagedListConfig: PagedList.Config): ThreadLivePageResult { + monarchy.awaitTransaction { realm -> + realm.where().findAll().deleteAllFromRealm() + } + + val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> + realm + .where().equalTo(ThreadSummaryEntityFields.PAGE.ROOM_ID, roomId) + .sort(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.ORIGIN_SERVER_TS, Sort.DESCENDING) + } + + val dataSourceFactory = realmDataSourceFactory.map { + threadSummaryMapper.map(it) + } + + val boundaries = MutableLiveData(ResultBoundaries()) + + val builder = LivePagedListBuilder(dataSourceFactory, pagedListConfig).also { + it.setBoundaryCallback(object : PagedList.BoundaryCallback() { + override fun onItemAtEndLoaded(itemAtEnd: ThreadSummary) { + boundaries.postValue(boundaries.value?.copy(endLoaded = true)) } + + override fun onItemAtFrontLoaded(itemAtFront: ThreadSummary) { + boundaries.postValue(boundaries.value?.copy(frontLoaded = true)) + } + + override fun onZeroItemsLoaded() { + boundaries.postValue(boundaries.value?.copy(zeroItemLoaded = true)) + } + }) + } + + val livePagedList = monarchy.findAllPagedWithChanges( + realmDataSourceFactory, + builder + ) + return ThreadLivePageResult(livePagedList, boundaries) + } + + override suspend fun fetchThreadList(nextBatchId: String?, limit: Int, filter: ThreadFilter): FetchThreadsResult { + return fetchThreadSummariesTask.execute( + FetchThreadSummariesTask.Params( + roomId = roomId, + from = nextBatchId, + limit = limit, + filter = filter + ) ) } - override fun getAllThreadSummaries(): List { + override suspend fun getAllThreadSummaries(): List { return monarchy.fetchAllMappedSync( { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) }, { threadSummaryMapper.map(it) } @@ -81,12 +130,4 @@ internal class DefaultThreadsService @AssistedInject constructor( ) ) } - - override suspend fun fetchThreadSummaries() { - fetchThreadSummariesTask.execute( - FetchThreadSummariesTask.Params( - roomId = roomId - ) - ) - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt index 14098fd8b0..3cbe652076 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListController.kt @@ -19,24 +19,18 @@ package im.vector.app.features.home.room.threads.list.viewmodel import com.airbnb.epoxy.EpoxyController import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter -import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.threads.list.model.threadListItem -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.api.util.toMatrixItem -import org.matrix.android.sdk.api.util.toMatrixItemOrNull import javax.inject.Inject class ThreadListController @Inject constructor( private val avatarRenderer: AvatarRenderer, - private val stringProvider: StringProvider, private val dateFormatter: VectorDateFormatter, private val displayableEventFormatter: DisplayableEventFormatter, - private val session: Session ) : EpoxyController() { var listener: Listener? = null @@ -48,64 +42,7 @@ class ThreadListController @Inject constructor( requestModelBuild() } - override fun buildModels() = - when (session.homeServerCapabilitiesService().getHomeServerCapabilities().canUseThreading) { - true -> buildThreadSummaries() - false -> buildThreadList() - } - - /** - * Building thread summaries when homeserver supports threading. - */ - private fun buildThreadSummaries() { - val safeViewState = viewState ?: return - val host = this - safeViewState.threadSummaryList.invoke() - ?.filter { - if (safeViewState.shouldFilterThreads) { - it.isUserParticipating - } else { - true - } - } - ?.forEach { threadSummary -> - val date = dateFormatter.format(threadSummary.latestEvent?.originServerTs, DateFormatKind.ROOM_LIST) - val lastMessageFormatted = threadSummary.let { - displayableEventFormatter.formatThreadSummary( - event = it.latestEvent, - latestEdition = it.threadEditions.latestThreadEdition - ).toString() - } - val rootMessageFormatted = threadSummary.let { - displayableEventFormatter.formatThreadSummary( - event = it.rootEvent, - latestEdition = it.threadEditions.rootThreadEdition - ).toString() - } - threadListItem { - id(threadSummary.rootEvent?.eventId) - avatarRenderer(host.avatarRenderer) - matrixItem(threadSummary.rootThreadSenderInfo.toMatrixItem()) - title(threadSummary.rootThreadSenderInfo.displayName.orEmpty()) - date(date) - rootMessageDeleted(threadSummary.rootEvent?.isRedacted() ?: false) - // TODO refactor notifications that with the new thread summary - threadNotificationState(threadSummary.rootEvent?.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE) - rootMessage(rootMessageFormatted) - lastMessage(lastMessageFormatted) - lastMessageCounter(threadSummary.numberOfThreads.toString()) - lastMessageMatrixItem(threadSummary.latestThreadSenderInfo.toMatrixItemOrNull()) - itemClickListener { - host.listener?.onThreadSummaryClicked(threadSummary) - } - } - } - } - - /** - * Building local thread list when homeserver do not support threading. - */ - private fun buildThreadList() { + override fun buildModels() { val safeViewState = viewState ?: return val host = this safeViewState.rootThreadEventList.invoke() @@ -152,7 +89,6 @@ class ThreadListController @Inject constructor( } interface Listener { - fun onThreadSummaryClicked(threadSummary: ThreadSummary) fun onThreadListClicked(timelineEvent: TimelineEvent) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListPagedController.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListPagedController.kt new file mode 100644 index 0000000000..171b690a33 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListPagedController.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2021 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.threads.list.viewmodel + +import com.airbnb.epoxy.EpoxyModel +import com.airbnb.epoxy.paging.PagedListEpoxyController +import im.vector.app.core.date.DateFormatKind +import im.vector.app.core.date.VectorDateFormatter +import im.vector.app.core.utils.createUIHandler +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter +import im.vector.app.features.home.room.threads.list.model.ThreadListItem_ +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary +import org.matrix.android.sdk.api.session.threads.ThreadNotificationState +import org.matrix.android.sdk.api.util.toMatrixItem +import org.matrix.android.sdk.api.util.toMatrixItemOrNull +import javax.inject.Inject + +class ThreadListPagedController @Inject constructor( + private val avatarRenderer: AvatarRenderer, + private val dateFormatter: VectorDateFormatter, + private val displayableEventFormatter: DisplayableEventFormatter, +) : PagedListEpoxyController( + // Important it must match the PageList builder notify Looper + modelBuildingHandler = createUIHandler() +) { + + var listener: Listener? = null + + override fun buildItemModel(currentPosition: Int, item: ThreadSummary?): EpoxyModel<*> { + if (item == null) { + throw java.lang.NullPointerException() + } + val host = this + val date = dateFormatter.format(item.latestEvent?.originServerTs, DateFormatKind.ROOM_LIST) + val lastMessageFormatted = item.let { + displayableEventFormatter.formatThreadSummary( + event = it.latestEvent, + latestEdition = it.threadEditions.latestThreadEdition + ).toString() + } + val rootMessageFormatted = item.let { + displayableEventFormatter.formatThreadSummary( + event = it.rootEvent, + latestEdition = it.threadEditions.rootThreadEdition + ).toString() + } + + return ThreadListItem_() + .id(item.rootEvent?.eventId) + .avatarRenderer(host.avatarRenderer) + .matrixItem(item.rootThreadSenderInfo.toMatrixItem()) + .title(item.rootThreadSenderInfo.displayName.orEmpty()) + .date(date) + .rootMessageDeleted(item.rootEvent?.isRedacted() ?: false) + // TODO refactor notifications that with the new thread summary + .threadNotificationState(item.rootEvent?.threadDetails?.threadNotificationState ?: ThreadNotificationState.NO_NEW_MESSAGE) + .rootMessage(rootMessageFormatted) + .lastMessage(lastMessageFormatted) + .lastMessageCounter(item.numberOfThreads.toString()) + .lastMessageMatrixItem(item.latestThreadSenderInfo.toMatrixItemOrNull()) + .itemClickListener { + host.listener?.onThreadSummaryClicked(item) + } + } + + interface Listener { + fun onThreadSummaryClicked(threadSummary: ThreadSummary) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt index 4b7af330fb..7124727bb7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewModel.kt @@ -16,6 +16,11 @@ package im.vector.app.features.home.room.threads.list.viewmodel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.asFlow +import androidx.paging.PagedList import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.ViewModelContext @@ -29,23 +34,47 @@ import im.vector.app.features.analytics.AnalyticsTracker import im.vector.app.features.analytics.extensions.toAnalyticsInteraction import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.home.room.threads.list.views.ThreadListFragment +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.threads.FetchThreadsResult +import org.matrix.android.sdk.api.session.room.threads.ThreadFilter +import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent import org.matrix.android.sdk.flow.flow class ThreadListViewModel @AssistedInject constructor( @Assisted val initialState: ThreadListViewState, private val analyticsTracker: AnalyticsTracker, - private val session: Session -) : - VectorViewModel(initialState) { + private val session: Session, +) : VectorViewModel(initialState) { private val room = session.getRoom(initialState.roomId) + private val defaultPagedListConfig = PagedList.Config.Builder() + .setPageSize(20) + .setInitialLoadSizeHint(40) + .setEnablePlaceholders(false) + .setPrefetchDistance(10) + .build() + + private var nextBatchId: String? = null + private var hasReachedEnd: Boolean = false + private var boundariesJob: Job? = null + + private var livePagedList: LiveData>? = null + private val _threadsLivePagedList = MutableLiveData>() + val threadsLivePagedList: LiveData> = _threadsLivePagedList + private val internalPagedListObserver = Observer> { + _threadsLivePagedList.postValue(it) + setLoading(false) + } + @AssistedFactory interface Factory { fun create(initialState: ThreadListViewState): ThreadListViewModel @@ -54,7 +83,7 @@ class ThreadListViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory { @JvmStatic - override fun create(viewModelContext: ViewModelContext, state: ThreadListViewState): ThreadListViewModel? { + override fun create(viewModelContext: ViewModelContext, state: ThreadListViewState): ThreadListViewModel { val fragment: ThreadListFragment = (viewModelContext as FragmentViewModelContext).fragment() return fragment.threadListViewModelFactory.create(state) } @@ -72,7 +101,7 @@ class ThreadListViewModel @AssistedInject constructor( private fun fetchAndObserveThreads() { when (session.homeServerCapabilitiesService().getHomeServerCapabilities().canUseThreading) { true -> { - fetchThreadList() + setLoading(true) observeThreadSummaries() } false -> observeThreadsList() @@ -82,14 +111,33 @@ class ThreadListViewModel @AssistedInject constructor( /** * Observing thread summaries when homeserver support threading. */ - private fun observeThreadSummaries() { - room?.flow() - ?.liveThreadSummaries() - ?.map { room.threadsService().enhanceThreadWithEditions(it) } - ?.flowOn(room.coroutineDispatchers.io) - ?.execute { asyncThreads -> - copy(threadSummaryList = asyncThreads) - } + private fun observeThreadSummaries() = withState { state -> + viewModelScope.launch { + nextBatchId = null + hasReachedEnd = false + + livePagedList?.removeObserver(internalPagedListObserver) + + room?.threadsService() + ?.getPagedThreadsList(state.shouldFilterThreads, defaultPagedListConfig)?.let { result -> + livePagedList = result.livePagedList + + livePagedList?.observeForever(internalPagedListObserver) + + boundariesJob = result.liveBoundaries.asFlow() + .onEach { + if (it.endLoaded) { + if (!hasReachedEnd) { + fetchNextPage() + } + } + } + .launchIn(viewModelScope) + } + + setLoading(true) + fetchNextPage() + } } /** @@ -111,14 +159,6 @@ class ThreadListViewModel @AssistedInject constructor( } } - private fun fetchThreadList() { - viewModelScope.launch { - setLoading(true) - room?.threadsService()?.fetchThreadSummaries() - setLoading(false) - } - } - private fun setLoading(isLoading: Boolean) { setState { copy(isLoading = isLoading) @@ -132,5 +172,30 @@ class ThreadListViewModel @AssistedInject constructor( setState { copy(shouldFilterThreads = shouldFilterThreads) } + + fetchAndObserveThreads() + } + + private suspend fun fetchNextPage() { + val filter = when (awaitState().shouldFilterThreads) { + true -> ThreadFilter.PARTICIPATED + false -> ThreadFilter.ALL + } + room?.threadsService()?.fetchThreadList( + nextBatchId = nextBatchId, + limit = defaultPagedListConfig.pageSize, + filter = filter, + ).let { result -> + when (result) { + is FetchThreadsResult.ReachedEnd -> { + hasReachedEnd = true + } + is FetchThreadsResult.ShouldFetchMore -> { + nextBatchId = result.nextBatch + } + else -> { + } + } + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt index 2328da0b8a..60ccfb59af 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/viewmodel/ThreadListViewState.kt @@ -20,11 +20,9 @@ import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.features.home.room.threads.arguments.ThreadListArgs -import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent data class ThreadListViewState( - val threadSummaryList: Async> = Uninitialized, val rootThreadEventList: Async> = Uninitialized, val shouldFilterThreads: Boolean = false, val isLoading: Boolean = false, diff --git a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt index f91fe9bd91..318c250906 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/threads/list/views/ThreadListFragment.kt @@ -40,6 +40,7 @@ import im.vector.app.features.home.room.threads.ThreadsActivity import im.vector.app.features.home.room.threads.arguments.ThreadListArgs import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListController +import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListPagedController import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewModel import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState import im.vector.app.features.rageshake.BugReporter @@ -52,12 +53,14 @@ import javax.inject.Inject @AndroidEntryPoint class ThreadListFragment : VectorBaseFragment(), + ThreadListPagedController.Listener, ThreadListController.Listener, VectorMenuProvider { @Inject lateinit var avatarRenderer: AvatarRenderer @Inject lateinit var bugReporter: BugReporter - @Inject lateinit var threadListController: ThreadListController + @Inject lateinit var threadListController: ThreadListPagedController + @Inject lateinit var legacyThreadListController: ThreadListController @Inject lateinit var threadListViewModelFactory: ThreadListViewModel.Factory private val threadListViewModel: ThreadListViewModel by fragmentViewModel() @@ -100,7 +103,7 @@ class ThreadListFragment : val filterBadge = filterIcon.findViewById(R.id.threadListFilterBadge) filterBadge.isVisible = state.shouldFilterThreads when (threadListViewModel.canHomeserverUseThreading()) { - true -> menu.findItem(R.id.menu_thread_list_filter).isVisible = !state.threadSummaryList.invoke().isNullOrEmpty() + true -> menu.findItem(R.id.menu_thread_list_filter).isVisible = true false -> menu.findItem(R.id.menu_thread_list_filter).isVisible = !state.rootThreadEventList.invoke().isNullOrEmpty() } } @@ -111,8 +114,18 @@ class ThreadListFragment : initToolbar() initTextConstants() initBetaFeedback() - views.threadListRecyclerView.configureWith(threadListController, TimelineItemAnimator(), hasFixedSize = false) - threadListController.listener = this + + if (threadListViewModel.canHomeserverUseThreading()) { + views.threadListRecyclerView.configureWith(threadListController, TimelineItemAnimator(), hasFixedSize = false) + threadListController.listener = this + + threadListViewModel.threadsLivePagedList.observe(viewLifecycleOwner) { threadsList -> + threadListController.submitList(threadsList) + } + } else { + views.threadListRecyclerView.configureWith(legacyThreadListController, TimelineItemAnimator(), hasFixedSize = false) + legacyThreadListController.listener = this + } } override fun onDestroyView() { @@ -144,7 +157,9 @@ class ThreadListFragment : override fun invalidate() = withState(threadListViewModel) { state -> invalidateOptionsMenu() renderEmptyStateIfNeeded(state) - threadListController.update(state) + if (!threadListViewModel.canHomeserverUseThreading()) { + legacyThreadListController.update(state) + } renderLoaderIfNeeded(state) } @@ -185,7 +200,7 @@ class ThreadListFragment : private fun renderEmptyStateIfNeeded(state: ThreadListViewState) { when (threadListViewModel.canHomeserverUseThreading()) { - true -> views.threadListEmptyConstraintLayout.isVisible = state.threadSummaryList.invoke().isNullOrEmpty() + true -> views.threadListEmptyConstraintLayout.isVisible = false false -> views.threadListEmptyConstraintLayout.isVisible = state.rootThreadEventList.invoke().isNullOrEmpty() } }