thread list loading (#7766)

This commit is contained in:
Nikita Fedrunov 2022-12-14 18:56:16 +01:00 committed by GitHub
parent c74ea2dd16
commit cf3abd6562
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 526 additions and 175 deletions

1
changelog.d/5819.misc Normal file
View file

@ -0,0 +1 @@
[Threads] - added API to fetch threads list from the server instead of building it locally from events

View file

@ -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.model.RoomSummary
import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState 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.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.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
@ -119,13 +118,6 @@ class FlowRoom(private val room: Room) {
return room.roomPushRuleService().getLiveRoomNotificationState().asFlow() return room.roomPushRuleService().getLiveRoomNotificationState().asFlow()
} }
fun liveThreadSummaries(): Flow<List<ThreadSummary>> {
return room.threadsService().getAllThreadSummariesLive().asFlow()
.startWith(room.coroutineDispatchers.io) {
room.threadsService().getAllThreadSummaries()
}
}
fun liveThreadList(): Flow<List<ThreadRootEvent>> { fun liveThreadList(): Flow<List<ThreadRootEvent>> {
return room.threadsLocalService().getAllThreadsLive().asFlow() return room.threadsLocalService().getAllThreadsLive().asFlow()
.startWith(room.coroutineDispatchers.io) { .startWith(room.coroutineDispatchers.io) {

View file

@ -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()
}

View file

@ -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,
}

View file

@ -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<PagedList<ThreadSummary>>,
val liveBoundaries: LiveData<ResultBoundaries>
)

View file

@ -16,7 +16,7 @@
package org.matrix.android.sdk.api.session.room.threads 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 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 { interface ThreadsService {
/** suspend fun getPagedThreadsList(userParticipating: Boolean, pagedListConfig: PagedList.Config): ThreadLivePageResult
* Returns a [LiveData] list of all the [ThreadSummary] that exists at the room level.
*/ suspend fun fetchThreadList(nextBatchId: String?, limit: Int, filter: ThreadFilter = ThreadFilter.ALL): FetchThreadsResult
fun getAllThreadSummariesLive(): LiveData<List<ThreadSummary>>
/** /**
* Returns a list of all the [ThreadSummary] that exists at the room level. * Returns a list of all the [ThreadSummary] that exists at the room level.
*/ */
fun getAllThreadSummaries(): List<ThreadSummary> suspend fun getAllThreadSummaries(): List<ThreadSummary>
/** /**
* Enhance the provided ThreadSummary[List] by adding the latest * 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 * @param limit defines the number of max results the api will respond with
*/ */
suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int) 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()
} }

View file

@ -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.MigrateSessionTo043
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo044 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.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.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject import javax.inject.Inject
@ -70,7 +71,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer private val normalizer: Normalizer
) : MatrixRealmMigration( ) : MatrixRealmMigration(
dbName = "Session", dbName = "Session",
schemaVersion = 45L, schemaVersion = 46L,
) { ) {
/** /**
* Forces all RealmSessionStoreMigration instances to be equal. * Forces all RealmSessionStoreMigration instances to be equal.
@ -125,5 +126,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 43) MigrateSessionTo043(realm).perform() if (oldVersion < 43) MigrateSessionTo043(realm).perform()
if (oldVersion < 44) MigrateSessionTo044(realm).perform() if (oldVersion < 44) MigrateSessionTo044(realm).perform()
if (oldVersion < 45) MigrateSessionTo045(realm).perform() if (oldVersion < 45) MigrateSessionTo045(realm).perform()
if (oldVersion < 46) MigrateSessionTo046(realm).perform()
} }
} }

View file

@ -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.EventInsertType
import org.matrix.android.sdk.internal.database.model.RoomEntity 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.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.ThreadSummaryEntity
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields 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.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.getOrCreate
import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.getOrNull
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
@ -113,16 +115,16 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate(
userId: String, userId: String,
cryptoService: CryptoService? = null, cryptoService: CryptoService? = null,
currentTimeMillis: Long, currentTimeMillis: Long,
) { ): ThreadSummaryEntity? {
when (threadSummaryType) { when (threadSummaryType) {
ThreadSummaryUpdateType.REPLACE -> { ThreadSummaryUpdateType.REPLACE -> {
rootThreadEvent?.eventId ?: return rootThreadEvent?.eventId ?: return null
rootThreadEvent.senderId ?: return 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 // 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 { val threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEvent.eventId).also {
Timber.i("###THREADS ThreadSummaryHelper REPLACE eventId:${it.rootThreadEventId} ") Timber.i("###THREADS ThreadSummaryHelper REPLACE eventId:${it.rootThreadEventId} ")
@ -153,12 +155,13 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate(
) )
roomEntity.addIfNecessary(threadSummary) roomEntity.addIfNecessary(threadSummary)
return threadSummary
} }
ThreadSummaryUpdateType.ADD -> { ThreadSummaryUpdateType.ADD -> {
val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return null
Timber.i("###THREADS ThreadSummaryHelper ADD for root eventId:$rootThreadEventId") 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) { if (threadSummary != null) {
// ThreadSummary exists so lets add the latest event // ThreadSummary exists so lets add the latest event
Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId exists, lets update latest thread 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") Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId do not exists, lets try to create one")
threadEventEntity.findRootThreadEvent()?.let { rootThreadEventEntity -> threadEventEntity.findRootThreadEvent()?.let { rootThreadEventEntity ->
// Root thread event entity exists so lets create a new record // 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( it.updateThreadSummary(
rootThreadEventEntity = rootThreadEventEntity, rootThreadEventEntity = rootThreadEventEntity,
numberOfThreads = 1, numberOfThreads = 1,
@ -183,7 +186,12 @@ internal fun ThreadSummaryEntity.Companion.createOrUpdate(
roomEntity.addIfNecessary(it) roomEntity.addIfNecessary(it)
} }
} }
threadSummary?.let {
ThreadListPageEntity.get(realm, roomId)?.threadSummaries?.add(it)
}
} }
return threadSummary
} }
} }
} }

View file

@ -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")!!)
}
}

View file

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.database.model
import io.realm.annotations.RealmModule import io.realm.annotations.RealmModule
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity 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.presence.UserPresenceEntity
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.ThreadSummaryEntity
/** /**
@ -72,6 +73,7 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit
UserPresenceEntity::class, UserPresenceEntity::class,
ThreadSummaryEntity::class, ThreadSummaryEntity::class,
SyncFilterParamsEntity::class, SyncFilterParamsEntity::class,
ThreadListPageEntity::class
] ]
) )
internal class SessionRealmModule internal class SessionRealmModule

View file

@ -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<ThreadSummaryEntity> = RealmList()
) : RealmObject() {
companion object
}

View file

@ -40,5 +40,8 @@ internal open class ThreadSummaryEntity(
@LinkingObjects("threadSummaries") @LinkingObjects("threadSummaries")
val room: RealmResults<RoomEntity>? = null val room: RealmResults<RoomEntity>? = null
@LinkingObjects("threadSummaries")
val page: RealmResults<ThreadListPageEntity>? = null
companion object companion object
} }

View file

@ -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<ThreadListPageEntity>().equalTo(ThreadListPageEntityFields.ROOM_ID, roomId).findFirst()
}
internal fun ThreadListPageEntity.Companion.getOrCreate(realm: Realm, roomId: String): ThreadListPageEntity {
return get(realm, roomId) ?: realm.createObject(roomId)
}

View file

@ -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.membership.threepid.ThreePidInviteBody
import org.matrix.android.sdk.internal.session.room.read.ReadBody 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.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.reporting.ReportContentBody
import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.session.room.send.SendResponse
import org.matrix.android.sdk.internal.session.room.tags.TagBody import org.matrix.android.sdk.internal.session.room.tags.TagBody
@ -464,4 +465,12 @@ internal interface RoomAPI {
@Path("roomIdOrAlias") roomidOrAlias: String, @Path("roomIdOrAlias") roomidOrAlias: String,
@Query("via") viaServers: List<String>? @Query("via") viaServers: List<String>?
): RoomStrippedState ): 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
} }

View file

@ -16,37 +16,38 @@
package org.matrix.android.sdk.internal.session.room.relation.threads package org.matrix.android.sdk.internal.session.room.relation.threads
import com.zhuinden.monarchy.Monarchy 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.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.api.session.room.threads.model.ThreadSummaryUpdateType
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
import org.matrix.android.sdk.internal.database.helper.createOrUpdate 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.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.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.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest 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.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.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.util.time.Clock import org.matrix.android.sdk.internal.util.time.Clock
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
/*** /***
* This class is responsible to Fetch all the thread in the current room, * 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. * To fetch all threads in a room, the /messages API is used with newly added filtering options.
*/ */
internal interface FetchThreadSummariesTask : Task<FetchThreadSummariesTask.Params, DefaultFetchThreadSummariesTask.Result> { internal interface FetchThreadSummariesTask : Task<FetchThreadSummariesTask.Params, FetchThreadsResult> {
data class Params( data class Params(
val roomId: String, val roomId: String,
val from: String = "", val from: String? = null,
val limit: Int = 500, val limit: Int = 5,
val isUserParticipating: Boolean = true val filter: ThreadFilter? = null,
) )
} }
@ -59,39 +60,43 @@ internal class DefaultFetchThreadSummariesTask @Inject constructor(
private val clock: Clock, private val clock: Clock,
) : FetchThreadSummariesTask { ) : FetchThreadSummariesTask {
override suspend fun execute(params: FetchThreadSummariesTask.Params): Result { override suspend fun execute(params: FetchThreadSummariesTask.Params): FetchThreadsResult {
val filter = FilterFactory.createThreadsFilter( val response = executeRequest(globalErrorReceiver) {
numberOfEvents = params.limit, roomAPI.getThreadsList(
userId = if (params.isUserParticipating) userId else null roomId = params.roomId,
).toJSONString() include = params.filter?.toString()?.lowercase(),
from = params.from,
val response = executeRequest( limit = params.limit
globalErrorReceiver, )
canRetry = true
) {
roomAPI.getRoomMessagesFrom(params.roomId, params.from, PaginationDirection.BACKWARDS.value, params.limit, filter)
} }
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( private suspend fun handleResponse(
response: PaginationResponse, response: ThreadSummariesResponse,
params: FetchThreadSummariesTask.Params params: FetchThreadSummariesTask.Params
): Result { ) {
val rootThreadList = response.events val rootThreadList = response.chunk
val threadSummaries = RealmList<ThreadSummaryEntity>()
monarchy.awaitTransaction { realm -> monarchy.awaitTransaction { realm ->
val roomEntity = RoomEntity.where(realm, roomId = params.roomId).findFirst() ?: return@awaitTransaction val roomEntity = RoomEntity.where(realm, roomId = params.roomId).findFirst() ?: return@awaitTransaction
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>() val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
for (rootThreadEvent in rootThreadList) { for (rootThreadEvent in rootThreadList) {
if (rootThreadEvent.eventId == null || rootThreadEvent.senderId == null || rootThreadEvent.type == null) { if (rootThreadEvent.eventId == null || rootThreadEvent.senderId == null || rootThreadEvent.type == null) {
continue continue
} }
ThreadSummaryEntity.createOrUpdate( val threadSummary = ThreadSummaryEntity.createOrUpdate(
threadSummaryType = ThreadSummaryUpdateType.REPLACE, threadSummaryType = ThreadSummaryUpdateType.REPLACE,
realm = realm, realm = realm,
roomId = params.roomId, roomId = params.roomId,
@ -102,14 +107,16 @@ internal class DefaultFetchThreadSummariesTask @Inject constructor(
cryptoService = cryptoService, cryptoService = cryptoService,
currentTimeMillis = clock.epochMillis(), 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
} }
} }

View file

@ -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<Event>,
@Json(name = "next_batch") val nextBatch: String?,
@Json(name = "prev_batch") val prevBatch: String?
)

View file

@ -16,32 +16,39 @@
package org.matrix.android.sdk.internal.session.room.threads 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 com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.realm.Realm 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.ThreadsService
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary 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.enhanceWithEditions
import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId 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.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.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.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.FetchThreadSummariesTask
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.util.awaitTransaction
internal class DefaultThreadsService @AssistedInject constructor( internal class DefaultThreadsService @AssistedInject constructor(
@Assisted private val roomId: String, @Assisted private val roomId: String,
@UserId private val userId: String,
private val fetchThreadTimelineTask: FetchThreadTimelineTask, private val fetchThreadTimelineTask: FetchThreadTimelineTask,
private val fetchThreadSummariesTask: FetchThreadSummariesTask,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val timelineEventMapper: TimelineEventMapper, private val threadSummaryMapper: ThreadSummaryMapper,
private val threadSummaryMapper: ThreadSummaryMapper private val fetchThreadSummariesTask: FetchThreadSummariesTask,
) : ThreadsService { ) : ThreadsService {
@AssistedFactory @AssistedFactory
@ -49,16 +56,58 @@ internal class DefaultThreadsService @AssistedInject constructor(
fun create(roomId: String): DefaultThreadsService fun create(roomId: String): DefaultThreadsService
} }
override fun getAllThreadSummariesLive(): LiveData<List<ThreadSummary>> { override suspend fun getPagedThreadsList(userParticipating: Boolean, pagedListConfig: PagedList.Config): ThreadLivePageResult {
return monarchy.findAllMappedWithChanges( monarchy.awaitTransaction { realm ->
{ ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) }, realm.where<ThreadListPageEntity>().findAll().deleteAllFromRealm()
{ }
threadSummaryMapper.map(it)
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
realm
.where<ThreadSummaryEntity>().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<ThreadSummary>() {
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<ThreadSummary> { override suspend fun getAllThreadSummaries(): List<ThreadSummary> {
return monarchy.fetchAllMappedSync( return monarchy.fetchAllMappedSync(
{ ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) }, { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) },
{ threadSummaryMapper.map(it) } { threadSummaryMapper.map(it) }
@ -81,12 +130,4 @@ internal class DefaultThreadsService @AssistedInject constructor(
) )
) )
} }
override suspend fun fetchThreadSummaries() {
fetchThreadSummariesTask.execute(
FetchThreadSummariesTask.Params(
roomId = roomId
)
)
}
} }

View file

@ -19,24 +19,18 @@ package im.vector.app.features.home.room.threads.list.viewmodel
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter 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.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
import im.vector.app.features.home.room.threads.list.model.threadListItem 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.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.api.util.toMatrixItemOrNull
import javax.inject.Inject import javax.inject.Inject
class ThreadListController @Inject constructor( class ThreadListController @Inject constructor(
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider,
private val dateFormatter: VectorDateFormatter, private val dateFormatter: VectorDateFormatter,
private val displayableEventFormatter: DisplayableEventFormatter, private val displayableEventFormatter: DisplayableEventFormatter,
private val session: Session
) : EpoxyController() { ) : EpoxyController() {
var listener: Listener? = null var listener: Listener? = null
@ -48,64 +42,7 @@ class ThreadListController @Inject constructor(
requestModelBuild() requestModelBuild()
} }
override fun buildModels() = 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() {
val safeViewState = viewState ?: return val safeViewState = viewState ?: return
val host = this val host = this
safeViewState.rootThreadEventList.invoke() safeViewState.rootThreadEventList.invoke()
@ -152,7 +89,6 @@ class ThreadListController @Inject constructor(
} }
interface Listener { interface Listener {
fun onThreadSummaryClicked(threadSummary: ThreadSummary)
fun onThreadListClicked(timelineEvent: TimelineEvent) fun onThreadListClicked(timelineEvent: TimelineEvent)
} }
} }

View file

@ -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<ThreadSummary>(
// 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)
}
}

View file

@ -16,6 +16,11 @@
package im.vector.app.features.home.room.threads.list.viewmodel 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.FragmentViewModelContext
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.ViewModelContext 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.extensions.toAnalyticsInteraction
import im.vector.app.features.analytics.plan.Interaction import im.vector.app.features.analytics.plan.Interaction
import im.vector.app.features.home.room.threads.list.views.ThreadListFragment import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoom 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.api.session.threads.ThreadTimelineEvent
import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.flow
class ThreadListViewModel @AssistedInject constructor( class ThreadListViewModel @AssistedInject constructor(
@Assisted val initialState: ThreadListViewState, @Assisted val initialState: ThreadListViewState,
private val analyticsTracker: AnalyticsTracker, private val analyticsTracker: AnalyticsTracker,
private val session: Session private val session: Session,
) : ) : VectorViewModel<ThreadListViewState, EmptyAction, EmptyViewEvents>(initialState) {
VectorViewModel<ThreadListViewState, EmptyAction, EmptyViewEvents>(initialState) {
private val room = session.getRoom(initialState.roomId) 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<PagedList<ThreadSummary>>? = null
private val _threadsLivePagedList = MutableLiveData<PagedList<ThreadSummary>>()
val threadsLivePagedList: LiveData<PagedList<ThreadSummary>> = _threadsLivePagedList
private val internalPagedListObserver = Observer<PagedList<ThreadSummary>> {
_threadsLivePagedList.postValue(it)
setLoading(false)
}
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(initialState: ThreadListViewState): ThreadListViewModel fun create(initialState: ThreadListViewState): ThreadListViewModel
@ -54,7 +83,7 @@ class ThreadListViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<ThreadListViewModel, ThreadListViewState> { companion object : MavericksViewModelFactory<ThreadListViewModel, ThreadListViewState> {
@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: ThreadListViewState): ThreadListViewModel? { override fun create(viewModelContext: ViewModelContext, state: ThreadListViewState): ThreadListViewModel {
val fragment: ThreadListFragment = (viewModelContext as FragmentViewModelContext).fragment() val fragment: ThreadListFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.threadListViewModelFactory.create(state) return fragment.threadListViewModelFactory.create(state)
} }
@ -72,7 +101,7 @@ class ThreadListViewModel @AssistedInject constructor(
private fun fetchAndObserveThreads() { private fun fetchAndObserveThreads() {
when (session.homeServerCapabilitiesService().getHomeServerCapabilities().canUseThreading) { when (session.homeServerCapabilitiesService().getHomeServerCapabilities().canUseThreading) {
true -> { true -> {
fetchThreadList() setLoading(true)
observeThreadSummaries() observeThreadSummaries()
} }
false -> observeThreadsList() false -> observeThreadsList()
@ -82,14 +111,33 @@ class ThreadListViewModel @AssistedInject constructor(
/** /**
* Observing thread summaries when homeserver support threading. * Observing thread summaries when homeserver support threading.
*/ */
private fun observeThreadSummaries() { private fun observeThreadSummaries() = withState { state ->
room?.flow() viewModelScope.launch {
?.liveThreadSummaries() nextBatchId = null
?.map { room.threadsService().enhanceThreadWithEditions(it) } hasReachedEnd = false
?.flowOn(room.coroutineDispatchers.io)
?.execute { asyncThreads -> livePagedList?.removeObserver(internalPagedListObserver)
copy(threadSummaryList = asyncThreads)
} 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) { private fun setLoading(isLoading: Boolean) {
setState { setState {
copy(isLoading = isLoading) copy(isLoading = isLoading)
@ -132,5 +172,30 @@ class ThreadListViewModel @AssistedInject constructor(
setState { setState {
copy(shouldFilterThreads = shouldFilterThreads) 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 -> {
}
}
}
} }
} }

View file

@ -20,11 +20,9 @@ import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.home.room.threads.arguments.ThreadListArgs 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 import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent
data class ThreadListViewState( data class ThreadListViewState(
val threadSummaryList: Async<List<ThreadSummary>> = Uninitialized,
val rootThreadEventList: Async<List<ThreadTimelineEvent>> = Uninitialized, val rootThreadEventList: Async<List<ThreadTimelineEvent>> = Uninitialized,
val shouldFilterThreads: Boolean = false, val shouldFilterThreads: Boolean = false,
val isLoading: Boolean = false, val isLoading: Boolean = false,

View file

@ -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.ThreadListArgs
import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs 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.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.ThreadListViewModel
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState
import im.vector.app.features.rageshake.BugReporter import im.vector.app.features.rageshake.BugReporter
@ -52,12 +53,14 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ThreadListFragment : class ThreadListFragment :
VectorBaseFragment<FragmentThreadListBinding>(), VectorBaseFragment<FragmentThreadListBinding>(),
ThreadListPagedController.Listener,
ThreadListController.Listener, ThreadListController.Listener,
VectorMenuProvider { VectorMenuProvider {
@Inject lateinit var avatarRenderer: AvatarRenderer @Inject lateinit var avatarRenderer: AvatarRenderer
@Inject lateinit var bugReporter: BugReporter @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 @Inject lateinit var threadListViewModelFactory: ThreadListViewModel.Factory
private val threadListViewModel: ThreadListViewModel by fragmentViewModel() private val threadListViewModel: ThreadListViewModel by fragmentViewModel()
@ -100,7 +103,7 @@ class ThreadListFragment :
val filterBadge = filterIcon.findViewById<View>(R.id.threadListFilterBadge) val filterBadge = filterIcon.findViewById<View>(R.id.threadListFilterBadge)
filterBadge.isVisible = state.shouldFilterThreads filterBadge.isVisible = state.shouldFilterThreads
when (threadListViewModel.canHomeserverUseThreading()) { 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() false -> menu.findItem(R.id.menu_thread_list_filter).isVisible = !state.rootThreadEventList.invoke().isNullOrEmpty()
} }
} }
@ -111,8 +114,18 @@ class ThreadListFragment :
initToolbar() initToolbar()
initTextConstants() initTextConstants()
initBetaFeedback() 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() { override fun onDestroyView() {
@ -144,7 +157,9 @@ class ThreadListFragment :
override fun invalidate() = withState(threadListViewModel) { state -> override fun invalidate() = withState(threadListViewModel) { state ->
invalidateOptionsMenu() invalidateOptionsMenu()
renderEmptyStateIfNeeded(state) renderEmptyStateIfNeeded(state)
threadListController.update(state) if (!threadListViewModel.canHomeserverUseThreading()) {
legacyThreadListController.update(state)
}
renderLoaderIfNeeded(state) renderLoaderIfNeeded(state)
} }
@ -185,7 +200,7 @@ class ThreadListFragment :
private fun renderEmptyStateIfNeeded(state: ThreadListViewState) { private fun renderEmptyStateIfNeeded(state: ThreadListViewState) {
when (threadListViewModel.canHomeserverUseThreading()) { when (threadListViewModel.canHomeserverUseThreading()) {
true -> views.threadListEmptyConstraintLayout.isVisible = state.threadSummaryList.invoke().isNullOrEmpty() true -> views.threadListEmptyConstraintLayout.isVisible = false
false -> views.threadListEmptyConstraintLayout.isVisible = state.rootThreadEventList.invoke().isNullOrEmpty() false -> views.threadListEmptyConstraintLayout.isVisible = state.rootThreadEventList.invoke().isNullOrEmpty()
} }
} }