Introduce live thread summaries using the enhanced /messages API from MSC 3440

Add capabilities to support local thread list to not supported servers
This commit is contained in:
ariskotsomitopoulos 2022-02-18 17:21:10 +02:00
parent 830c38f50b
commit 83088bbe5a
47 changed files with 1221 additions and 139 deletions

View file

@ -28,6 +28,7 @@ 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
@ -101,13 +102,18 @@ class FlowRoom(private val room: Room) {
return room.getLiveRoomNotificationState().asFlow() return room.getLiveRoomNotificationState().asFlow()
} }
fun liveThreadSummaries(): Flow<List<ThreadSummary>> {
return room.getAllThreadSummariesLive().asFlow()
.startWith(room.coroutineDispatchers.io) {
room.getAllThreadSummaries()
}
}
fun liveThreadList(): Flow<List<ThreadRootEvent>> { fun liveThreadList(): Flow<List<ThreadRootEvent>> {
return room.getAllThreadsLive().asFlow() return room.getAllThreadsLive().asFlow()
.startWith(room.coroutineDispatchers.io) { .startWith(room.coroutineDispatchers.io) {
room.getAllThreads() room.getAllThreads()
} }
} }
fun liveLocalUnreadThreadList(): Flow<List<ThreadRootEvent>> { fun liveLocalUnreadThreadList(): Flow<List<ThreadRootEvent>> {
return room.getMarkedThreadNotificationsLive().asFlow() return room.getMarkedThreadNotificationsLive().asFlow()
.startWith(room.coroutineDispatchers.io) { .startWith(room.coroutineDispatchers.io) {

View file

@ -49,5 +49,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class AggregatedRelations( data class AggregatedRelations(
@Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null, @Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null,
@Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null,
@Json(name = RelationType.IO_THREAD) val latestThread: LatestThreadUnsignedRelation? = null
) )

View file

@ -0,0 +1,30 @@
/*
* 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.events.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class LatestThreadUnsignedRelation(
override val limited: Boolean? = false,
override val count: Int? = 0,
@Json(name = "latest_event")
val event: Event? = null,
@Json(name = "current_user_participated")
val isUserParticipating: Boolean? = false
) : UnsignedRelationInfo

View file

@ -50,7 +50,11 @@ data class HomeServerCapabilities(
* This capability describes the default and available room versions a server supports, and at what level of stability. * This capability describes the default and available room versions a server supports, and at what level of stability.
* Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms. * Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms.
*/ */
val roomVersions: RoomVersionCapabilities? = null val roomVersions: RoomVersionCapabilities? = null,
/**
* True if the home server support threading
*/
var canUseThreading: Boolean = false
) { ) {
enum class RoomCapabilitySupport { enum class RoomCapabilitySupport {

View file

@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.send.SendService
import org.matrix.android.sdk.api.session.room.state.StateService import org.matrix.android.sdk.api.session.room.state.StateService
import org.matrix.android.sdk.api.session.room.tags.TagsService import org.matrix.android.sdk.api.session.room.tags.TagsService
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.local.ThreadsLocalService
import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.timeline.TimelineService
import org.matrix.android.sdk.api.session.room.typing.TypingService import org.matrix.android.sdk.api.session.room.typing.TypingService
import org.matrix.android.sdk.api.session.room.uploads.UploadsService import org.matrix.android.sdk.api.session.room.uploads.UploadsService
@ -47,6 +48,7 @@ import org.matrix.android.sdk.api.util.Optional
interface Room : interface Room :
TimelineService, TimelineService,
ThreadsService, ThreadsService,
ThreadsLocalService,
SendService, SendService,
DraftService, DraftService,
ReadService, ReadService,

View file

@ -163,13 +163,4 @@ interface RelationService {
autoMarkdown: Boolean = false, autoMarkdown: Boolean = false,
formattedText: String? = null, formattedText: String? = null,
eventReplied: TimelineEvent? = null): Cancelable? eventReplied: TimelineEvent? = null): Cancelable?
/**
* Get all the thread replies for the specified rootThreadEventId
* The return list will contain the original root thread event and all the thread replies to that event
* Note: We will use a large limit value in order to avoid using pagination until it would be 100% ready
* from the backend
* @param rootThreadEventId the root thread eventId
*/
suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean
} }

View file

@ -17,51 +17,43 @@
package org.matrix.android.sdk.api.session.room.threads package org.matrix.android.sdk.api.session.room.threads
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
/** /**
* This interface defines methods to interact with threads related features. * This interface defines methods to interact with thread related features.
* It's implemented at the room level within the main timeline. * It's the dynamic threads implementation and the homeserver must return
* a capability entry for threads. If the server do not support m.thread
* then [ThreadsLocalService] should be used instead
*/ */
interface ThreadsService { interface ThreadsService {
/** /**
* Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level * Returns a [LiveData] list of all the [ThreadSummary] that exists at the room level
*/ */
fun getAllThreadsLive(): LiveData<List<TimelineEvent>> fun getAllThreadSummariesLive(): LiveData<List<ThreadSummary>>
/** /**
* Returns a list of all the thread root TimelineEvents that exists at the room level * Returns a list of all the [ThreadSummary] that exists at the room level
*/ */
fun getAllThreads(): List<TimelineEvent> fun getAllThreadSummaries(): List<ThreadSummary>
/** /**
* Returns a [LiveData] list of all the marked unread threads that exists at the room level * Enhance the provided ThreadSummary[List] by adding the latest
*/
fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>>
/**
* Returns a list of all the marked unread threads that exists at the room level
*/
fun getMarkedThreadNotifications(): List<TimelineEvent>
/**
* Returns whether or not the current user is participating in the thread
* @param rootThreadEventId the eventId of the current thread
*/
fun isUserParticipatingInThread(rootThreadEventId: String): Boolean
/**
* Enhance the provided root thread TimelineEvent [List] by adding the latest
* message edition for that thread * message edition for that thread
* @return the enhanced [List] with edited updates * @return the enhanced [List] with edited updates
*/ */
fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent> fun enhanceWithEditions(threads: List<ThreadSummary>): List<ThreadSummary>
/** /**
* Marks the current thread as read in local DB. * Fetch all thread replies for the specified thread using the /relations api
* note: read receipts within threads are not yet supported with the API * @param rootThreadEventId the root thread eventId
* @param rootThreadEventId the root eventId of the current thread * @param from defines the token that will fetch from that position
* @param limit defines the number of max results the api will respond with
*/ */
suspend fun markThreadAsRead(rootThreadEventId: String) 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

@ -0,0 +1,68 @@
/*
* 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.local
import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
/**
* This interface defines methods to interact with thread related features.
* It's the local threads implementation and assumes that the homeserver
* do not support threads
*/
interface ThreadsLocalService {
/**
* Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level
*/
fun getAllThreadsLive(): LiveData<List<TimelineEvent>>
/**
* Returns a list of all the thread root TimelineEvents that exists at the room level
*/
fun getAllThreads(): List<TimelineEvent>
/**
* Returns a [LiveData] list of all the marked unread threads that exists at the room level
*/
fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>>
/**
* Returns a list of all the marked unread threads that exists at the room level
*/
fun getMarkedThreadNotifications(): List<TimelineEvent>
/**
* Returns whether or not the current user is participating in the thread
* @param rootThreadEventId the eventId of the current thread
*/
fun isUserParticipatingInThread(rootThreadEventId: String): Boolean
/**
* Enhance the provided root thread TimelineEvent [List] by adding the latest
* message edition for that thread
* @return the enhanced [List] with edited updates
*/
fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent>
/**
* Marks the current thread as read in local DB.
* note: read receipts within threads are not yet supported with the API
* @param rootThreadEventId the root eventId of the current thread
*/
suspend fun markThreadAsRead(rootThreadEventId: String)
}

View file

@ -0,0 +1,20 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.threads.model
data class ThreadEditions(var rootThreadEdition: String? = null,
var latestThreadEdition: String? = null)

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.threads.model
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
/**
* The main thread Summary model, mainly used to display the thread list
*/
data class ThreadSummary(val roomId: String,
val rootEvent: Event?,
val latestEvent: Event?,
val rootEventId: String,
val rootThreadSenderInfo: SenderInfo,
val latestThreadSenderInfo: SenderInfo,
val isUserParticipating: Boolean,
val numberOfThreads: Int,
val threadEditions: ThreadEditions = ThreadEditions())

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.threads.model
enum class ThreadSummaryUpdateType {
REPLACE,
ADD
}

View file

@ -17,6 +17,7 @@
package org.matrix.android.sdk.api.util package org.matrix.android.sdk.api.util
import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.group.model.GroupSummary import org.matrix.android.sdk.api.session.group.model.GroupSummary
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary 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
@ -178,6 +179,8 @@ fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName,
fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl) fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl)
fun SenderInfo.toMatrixItemOrNull() = tryOrNull { MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl) }
fun SpaceChildInfo.toMatrixItem() = if (roomType == RoomType.SPACE) { fun SpaceChildInfo.toMatrixItem() = if (roomType == RoomType.SPACE) {
MatrixItem.SpaceItem(childRoomId, name ?: canonicalAlias, avatarUrl) MatrixItem.SpaceItem(childRoomId, name ?: canonicalAlias, avatarUrl)
} else { } else {

View file

@ -44,6 +44,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo023
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo024 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo024
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo026 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo026
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo027
import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.Normalizer
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -58,7 +59,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
override fun equals(other: Any?) = other is RealmSessionStoreMigration override fun equals(other: Any?) = other is RealmSessionStoreMigration
override fun hashCode() = 1000 override fun hashCode() = 1000
val schemaVersion = 25L val schemaVersion = 27L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Realm Session from $oldVersion to $newVersion") Timber.d("Migrating Realm Session from $oldVersion to $newVersion")
@ -89,5 +90,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 24) MigrateSessionTo024(realm).perform() if (oldVersion < 24) MigrateSessionTo024(realm).perform()
if (oldVersion < 25) MigrateSessionTo025(realm).perform() if (oldVersion < 25) MigrateSessionTo025(realm).perform()
if (oldVersion < 26) MigrateSessionTo026(realm).perform() if (oldVersion < 26) MigrateSessionTo026(realm).perform()
if (oldVersion < 27) MigrateSessionTo027(realm).perform()
} }
} }

View file

@ -118,7 +118,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
return timelineEventEntity return timelineEventEntity
} }
private fun computeIsUnique( fun computeIsUnique(
realm: Realm, realm: Realm,
roomId: String, roomId: String,
isLastForward: Boolean, isLastForward: Boolean,

View file

@ -18,9 +18,16 @@ package org.matrix.android.sdk.internal.database.helper
import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.ChunkEntity
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.ThreadSummaryEntity
internal fun RoomEntity.addIfNecessary(chunkEntity: ChunkEntity) { internal fun RoomEntity.addIfNecessary(chunkEntity: ChunkEntity) {
if (!chunks.contains(chunkEntity)) { if (!chunks.contains(chunkEntity)) {
chunks.add(chunkEntity) chunks.add(chunkEntity)
} }
} }
internal fun RoomEntity.addIfNecessary(threadSummary: ThreadSummaryEntity) {
if (!threadSummaries.contains(threadSummary)) {
threadSummaries.add(threadSummary)
}
}

View file

@ -34,7 +34,7 @@ import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId import org.matrix.android.sdk.internal.database.query.whereRoomId
private typealias ThreadSummary = Pair<Int, TimelineEventEntity>? private typealias Summary = Pair<Int, TimelineEventEntity>?
/** /**
* Finds the root thread event and update it with the latest message summary along with the number * Finds the root thread event and update it with the latest message summary along with the number
@ -93,7 +93,7 @@ internal fun EventEntity.markEventAsRoot(
* @param rootThreadEventId The root eventId that will find the number of threads * @param rootThreadEventId The root eventId that will find the number of threads
* @return A ThreadSummary containing the counted threads and the latest event message * @return A ThreadSummary containing the counted threads and the latest event message
*/ */
internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): ThreadSummary { internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): Summary {
// Number of messages // Number of messages
val messages = TimelineEventEntity val messages = TimelineEventEntity
.whereRoomId(realm, roomId = roomId) .whereRoomId(realm, roomId = roomId)
@ -124,7 +124,7 @@ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId:
result ?: return null result ?: return null
return ThreadSummary(messages, result) return Summary(messages, result)
} }
/** /**

View file

@ -0,0 +1,332 @@
/*
* 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.helper
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.Sort
import io.realm.kotlin.createObject
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
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.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.getOrCreate
import org.matrix.android.sdk.internal.database.query.getOrNull
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
import timber.log.Timber
import java.util.UUID
internal fun ThreadSummaryEntity.updateThreadSummary(
rootThreadEventEntity: EventEntity,
numberOfThreads: Int?,
latestThreadEventEntity: EventEntity?,
isUserParticipating: Boolean,
roomMemberContentsByUser: HashMap<String, RoomMemberContent?>) {
updateThreadSummaryRootEvent(rootThreadEventEntity, roomMemberContentsByUser)
updateThreadSummaryLatestEvent(latestThreadEventEntity, roomMemberContentsByUser)
// Update latest event
// latestThreadEventEntity?.toTimelineEventEntity(roomMemberContentsByUser)?.let {
// Timber.i("###THREADS FetchThreadSummariesTask ThreadSummaryEntity updated latest event:${it.eventId} !")
// this.eventEntity?.threadSummaryLatestMessage = it
// }
// Update number of threads
this.isUserParticipating = isUserParticipating
numberOfThreads?.let {
// Update only when there is an actual value
this.numberOfThreads = it
}
}
/**
* Updates the root thread event properties
*/
internal fun ThreadSummaryEntity.updateThreadSummaryRootEvent(
rootThreadEventEntity: EventEntity,
roomMemberContentsByUser: HashMap<String, RoomMemberContent?>
) {
val roomId = rootThreadEventEntity.roomId
val rootThreadRoomMemberContent = roomMemberContentsByUser[rootThreadEventEntity.sender ?: ""]
this.rootThreadEventEntity = rootThreadEventEntity
this.rootThreadSenderAvatar = rootThreadRoomMemberContent?.avatarUrl
this.rootThreadSenderName = rootThreadRoomMemberContent?.displayName
this.rootThreadIsUniqueDisplayName = if (rootThreadRoomMemberContent?.displayName != null) {
computeIsUnique(realm, roomId, false, rootThreadRoomMemberContent, roomMemberContentsByUser)
} else {
true
}
}
/**
* Updates the latest thread event properties
*/
internal fun ThreadSummaryEntity.updateThreadSummaryLatestEvent(
latestThreadEventEntity: EventEntity?,
roomMemberContentsByUser: HashMap<String, RoomMemberContent?>
) {
val roomId = latestThreadEventEntity?.roomId ?: return
val latestThreadRoomMemberContent = roomMemberContentsByUser[latestThreadEventEntity.sender ?: ""]
this.latestThreadEventEntity = latestThreadEventEntity
this.latestThreadSenderAvatar = latestThreadRoomMemberContent?.avatarUrl
this.latestThreadSenderName = latestThreadRoomMemberContent?.displayName
this.latestThreadIsUniqueDisplayName = if (latestThreadRoomMemberContent?.displayName != null) {
computeIsUnique(realm, roomId, false, latestThreadRoomMemberContent, roomMemberContentsByUser)
} else {
true
}
}
private fun EventEntity.toTimelineEventEntity(roomMemberContentsByUser: HashMap<String, RoomMemberContent?>): TimelineEventEntity {
val roomId = roomId
val eventId = eventId
val localId = TimelineEventEntity.nextId(realm)
val senderId = sender ?: ""
val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
this.localId = localId
this.root = this@toTimelineEventEntity
this.eventId = eventId
this.roomId = roomId
this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
?.also { it.cleanUp(sender) }
this.ownedByThreadChunk = true // To skip it from the original event flow
val roomMemberContent = roomMemberContentsByUser[senderId]
this.senderAvatar = roomMemberContent?.avatarUrl
this.senderName = roomMemberContent?.displayName
isUniqueDisplayName = if (roomMemberContent?.displayName != null) {
computeIsUnique(realm, roomId, false, roomMemberContent, roomMemberContentsByUser)
} else {
true
}
}
return timelineEventEntity
}
internal fun ThreadSummaryEntity.Companion.createOrUpdate(
threadSummaryType: ThreadSummaryUpdateType,
realm: Realm,
roomId: String,
threadEventEntity: EventEntity? = null,
rootThreadEvent: Event? = null,
roomMemberContentsByUser: HashMap<String, RoomMemberContent?>,
roomEntity: RoomEntity,
userId: String,
cryptoService: CryptoService? = null
) {
when (threadSummaryType) {
ThreadSummaryUpdateType.REPLACE -> {
rootThreadEvent?.eventId ?: return
rootThreadEvent.senderId ?: return
val numberOfThreads = rootThreadEvent.unsignedData?.relations?.latestThread?.count ?: return
// Something is wrong with the server return
if (numberOfThreads <= 0) return
val threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEvent.eventId).also {
Timber.i("###THREADS ThreadSummaryHelper REPLACE eventId:${it.rootThreadEventId} ")
}
val rootThreadEventEntity = createEventEntity(roomId, rootThreadEvent, realm).also {
decryptIfNeeded(cryptoService, it, roomId)
}
val latestThreadEventEntity = createLatestEventEntity(roomId, rootThreadEvent, roomMemberContentsByUser, realm)?.also {
decryptIfNeeded(cryptoService, it, roomId)
}
val isUserParticipating = rootThreadEvent.unsignedData.relations.latestThread.isUserParticipating == true || rootThreadEvent.senderId == userId
roomMemberContentsByUser.addSenderState(realm, roomId, rootThreadEvent.senderId)
threadSummary.updateThreadSummary(
rootThreadEventEntity = rootThreadEventEntity,
numberOfThreads = numberOfThreads,
latestThreadEventEntity = latestThreadEventEntity,
isUserParticipating = isUserParticipating,
roomMemberContentsByUser = roomMemberContentsByUser
)
roomEntity.addIfNecessary(threadSummary)
}
ThreadSummaryUpdateType.ADD -> {
val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return
Timber.i("###THREADS ThreadSummaryHelper ADD for root eventId:$rootThreadEventId")
val 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.")
threadSummary.updateThreadSummaryLatestEvent(threadEventEntity, roomMemberContentsByUser)
threadSummary.numberOfThreads++
if (threadEventEntity.sender == userId) {
threadSummary.isUserParticipating = true
}
} else {
// ThreadSummary 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 ->
// Root thread event entity exists so lets create a new record
ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEventEntity.eventId).let {
it.updateThreadSummary(
rootThreadEventEntity = rootThreadEventEntity,
numberOfThreads = 1,
latestThreadEventEntity = threadEventEntity,
isUserParticipating = threadEventEntity.sender == userId,
roomMemberContentsByUser = roomMemberContentsByUser
)
roomEntity.addIfNecessary(it)
}
}
}
}
}
}
private fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEntity, roomId: String) {
cryptoService ?: return
val event = eventEntity.asDomain()
if (event.isEncrypted() && event.mxDecryptionResult == null && event.eventId != null) {
try {
Timber.i("###THREADS ThreadSummaryHelper request decryption for eventId:${event.eventId}")
// Event from sync does not have roomId, so add it to the event first
val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
// Save decryption result, to not decrypt every time we enter the thread list
eventEntity.setDecryptionResult(result)
} catch (e: MXCryptoError) {
if (e is MXCryptoError.Base) {
event.mCryptoError = e.errorType
event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
}
}
}
}
/**
* Request decryption
*/
private fun requestDecryption(eventDecryptor: TimelineEventDecryptor?, event: Event?) {
eventDecryptor ?: return
event ?: return
if (event.isEncrypted() &&
event.mxDecryptionResult == null && event.eventId != null) {
Timber.i("###THREADS ThreadSummaryHelper request decryption for eventId:${event.eventId}")
eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(event, UUID.randomUUID().toString()))
}
}
/**
* If we don't have any new state on this user, get it from db
*/
private fun HashMap<String, RoomMemberContent?>.addSenderState(realm: Realm, roomId: String, senderId: String) {
getOrPut(senderId) {
CurrentStateEventEntity
.getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)
?.root?.asDomain()
?.getFixedRoomMemberContent()
}
}
/**
* Create an EventEntity for the root thread event or get an existing one
*/
private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity {
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
}
/**
* Create an EventEntity for the latest thread event or get an existing one. Also update the user room member
* state
*/
private fun createLatestEventEntity(roomId: String, rootThreadEvent: Event, roomMemberContentsByUser: HashMap<String, RoomMemberContent?>, realm: Realm): EventEntity? {
return getLatestEvent(rootThreadEvent)?.let {
it.senderId?.let { senderId ->
roomMemberContentsByUser.addSenderState(realm, roomId, senderId)
}
createEventEntity(roomId, it, realm)
}
}
/**
* Returned the latest event message, if any
*/
private fun getLatestEvent(rootThreadEvent: Event): Event? {
return rootThreadEvent.unsignedData?.relations?.latestThread?.event
}
/**
* Find all ThreadSummaryEntity for the specified roomId, sorted by origin server
* note: Sorting cannot be provided by server, so we have to use that unstable property
* @param roomId The id of the room
*/
internal fun ThreadSummaryEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery<ThreadSummaryEntity> =
ThreadSummaryEntity
.where(realm, roomId = roomId)
.sort(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.ORIGIN_SERVER_TS, Sort.DESCENDING)
/**
* Enhance each [ThreadSummary] root and latest event with the equivalent decrypted text edition/replacement
*/
internal fun List<ThreadSummary>.enhanceWithEditions(realm: Realm, roomId: String): List<ThreadSummary> =
this.map {
it.addEditionIfNeeded(realm, roomId, true)
it.addEditionIfNeeded(realm, roomId, false)
it
}
private fun ThreadSummary.addEditionIfNeeded(realm: Realm, roomId: String, enhanceRoot: Boolean) {
val eventId = if (enhanceRoot) rootEventId else latestEvent?.eventId ?: return
EventAnnotationsSummaryEntity
.where(realm, roomId, eventId)
.findFirst()
?.editSummary
?.editions
?.lastOrNull()
?.eventId
?.let { editedEventId ->
TimelineEventEntity.where(realm, roomId, eventId = editedEventId).findFirst()?.let { editedEvent ->
if (enhanceRoot) {
threadEditions.rootThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary() ?: "(edited)"
} else {
threadEditions.latestThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary() ?: "(edited)"
}
}
}
}

View file

@ -41,7 +41,8 @@ internal object HomeServerCapabilitiesMapper {
maxUploadFileSize = entity.maxUploadFileSize, maxUploadFileSize = entity.maxUploadFileSize,
lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported, lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
defaultIdentityServerUrl = entity.defaultIdentityServerUrl, defaultIdentityServerUrl = entity.defaultIdentityServerUrl,
roomVersions = mapRoomVersion(entity.roomVersionsJson) roomVersions = mapRoomVersion(entity.roomVersionsJson),
canUseThreading = false // entity.canUseThreading
) )
} }

View file

@ -0,0 +1,48 @@
/*
* 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.mapper
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
import javax.inject.Inject
internal class ThreadSummaryMapper @Inject constructor() {
fun map(threadSummary: ThreadSummaryEntity): ThreadSummary {
return ThreadSummary(
roomId = threadSummary.room?.firstOrNull()?.roomId.orEmpty(),
rootEvent = threadSummary.rootThreadEventEntity?.asDomain(),
latestEvent = threadSummary.latestThreadEventEntity?.asDomain(),
rootEventId = threadSummary.rootThreadEventId,
rootThreadSenderInfo = SenderInfo(
userId = threadSummary.rootThreadEventEntity?.sender ?: "",
displayName = threadSummary.rootThreadSenderName,
isUniqueDisplayName = threadSummary.rootThreadIsUniqueDisplayName,
avatarUrl = threadSummary.rootThreadSenderAvatar
),
latestThreadSenderInfo = SenderInfo(
userId = threadSummary.latestThreadEventEntity?.sender ?: "",
displayName = threadSummary.latestThreadSenderName,
isUniqueDisplayName = threadSummary.latestThreadIsUniqueDisplayName,
avatarUrl = threadSummary.latestThreadSenderAvatar
),
isUserParticipating = threadSummary.isUserParticipating,
numberOfThreads = threadSummary.numberOfThreads
)
}
}

View file

@ -0,0 +1,49 @@
/*
* 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 io.realm.FieldAttribute
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
import org.matrix.android.sdk.internal.database.model.RoomEntityFields
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator
class MigrateSessionTo027(realm: DynamicRealm) : RealmMigrator(realm, 27) {
override fun doMigrate(realm: DynamicRealm) {
val eventEntity = realm.schema.get("EventEntity") ?: return
val threadSummaryEntity = realm.schema.create("ThreadSummaryEntity")
.addField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
.addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_NAME, String::class.java)
.addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_AVATAR, String::class.java)
.addField(ThreadSummaryEntityFields.ROOT_THREAD_IS_UNIQUE_DISPLAY_NAME, String::class.java)
.addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_NAME, String::class.java)
.addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_AVATAR, String::class.java)
.addField(ThreadSummaryEntityFields.LATEST_THREAD_IS_UNIQUE_DISPLAY_NAME, String::class.java)
.addField(ThreadSummaryEntityFields.NUMBER_OF_THREADS, Int::class.java)
.addField(ThreadSummaryEntityFields.IS_USER_PARTICIPATING, Boolean::class.java)
.addRealmObjectField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ENTITY.`$`, eventEntity)
.addRealmObjectField(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.`$`, eventEntity)
realm.schema.get("RoomEntity")
?.addRealmListField(RoomEntityFields.THREAD_SUMMARIES.`$`, threadSummaryEntity)
realm.schema.get("HomeServerCapabilitiesEntity")
?.addRealmListField(HomeServerCapabilitiesEntityFields.CAN_USE_THREADING, Boolean::class.java)
}
}

View file

@ -50,13 +50,18 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
companion object companion object
} }
internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRoot: Boolean) { internal fun ChunkEntity.deleteOnCascade(
deleteStateEvents: Boolean,
canDeleteRoot: Boolean) {
assertIsManaged() assertIsManaged()
if (deleteStateEvents) { if (deleteStateEvents) {
stateEvents.deleteAllFromRealm() stateEvents.deleteAllFromRealm()
} }
timelineEvents.clearWith { timelineEvents.clearWith {
val deleteRoot = canDeleteRoot && (it.root?.stateKey == null || deleteStateEvents) val deleteRoot = canDeleteRoot && (it.root?.stateKey == null || deleteStateEvents)
if (deleteRoot) {
room?.firstOrNull()?.removeThreadSummaryIfNeeded(it.eventId)
}
it.deleteOnCascade(deleteRoot) it.deleteOnCascade(deleteRoot)
} }
deleteFromRealm() deleteFromRealm()

View file

@ -34,14 +34,14 @@ internal open class EventEntity(@Index var eventId: String = "",
@Index var stateKey: String? = null, @Index var stateKey: String? = null,
var originServerTs: Long? = null, var originServerTs: Long? = null,
@Index var sender: String? = null, @Index var sender: String? = null,
// Can contain a serialized MatrixError // Can contain a serialized MatrixError
var sendStateDetails: String? = null, var sendStateDetails: String? = null,
var age: Long? = 0, var age: Long? = 0,
var unsignedData: String? = null, var unsignedData: String? = null,
var redacts: String? = null, var redacts: String? = null,
var decryptionResultJson: String? = null, var decryptionResultJson: String? = null,
var ageLocalTs: Long? = null, var ageLocalTs: Long? = null,
// Thread related, no need to create a new Entity for performance // Thread related, no need to create a new Entity for performance
@Index var isRootThread: Boolean = false, @Index var isRootThread: Boolean = false,
@Index var rootThreadEventId: String? = null, @Index var rootThreadEventId: String? = null,
var numberOfThreads: Int = 0, var numberOfThreads: Int = 0,

View file

@ -28,7 +28,8 @@ internal open class HomeServerCapabilitiesEntity(
var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN, var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN,
var lastVersionIdentityServerSupported: Boolean = false, var lastVersionIdentityServerSupported: Boolean = false,
var defaultIdentityServerUrl: String? = null, var defaultIdentityServerUrl: String? = null,
var lastUpdatedTimestamp: Long = 0L var lastUpdatedTimestamp: Long = 0L,
var canUseThreading: Boolean = false
) : RealmObject() { ) : RealmObject() {
companion object companion object

View file

@ -20,10 +20,14 @@ import io.realm.RealmList
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
import org.matrix.android.sdk.internal.database.query.findRootOrLatest
import org.matrix.android.sdk.internal.extensions.assertIsManaged
internal open class RoomEntity(@PrimaryKey var roomId: String = "", internal open class RoomEntity(@PrimaryKey var roomId: String = "",
var chunks: RealmList<ChunkEntity> = RealmList(), var chunks: RealmList<ChunkEntity> = RealmList(),
var sendingTimelineEvents: RealmList<TimelineEventEntity> = RealmList(), var sendingTimelineEvents: RealmList<TimelineEventEntity> = RealmList(),
var threadSummaries: RealmList<ThreadSummaryEntity> = RealmList(),
var accountData: RealmList<RoomAccountDataEntity> = RealmList() var accountData: RealmList<RoomAccountDataEntity> = RealmList()
) : RealmObject() { ) : RealmObject() {
@ -46,3 +50,10 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "",
} }
companion object companion object
} }
internal fun RoomEntity.removeThreadSummaryIfNeeded(eventId: String) {
assertIsManaged()
threadSummaries.findRootOrLatest(eventId)?.let {
threadSummaries.remove(it)
it.deleteFromRealm()
}
}

View file

@ -18,6 +18,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.presence.UserPresenceEntity import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
/** /**
* Realm module for Session * Realm module for Session
@ -66,6 +67,7 @@ import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntit
RoomAccountDataEntity::class, RoomAccountDataEntity::class,
SpaceChildSummaryEntity::class, SpaceChildSummaryEntity::class,
SpaceParentSummaryEntity::class, SpaceParentSummaryEntity::class,
UserPresenceEntity::class UserPresenceEntity::class,
ThreadSummaryEntity::class
]) ])
internal class SessionRealmModule internal class SessionRealmModule

View file

@ -0,0 +1,43 @@
/*
* 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.RealmObject
import io.realm.RealmResults
import io.realm.annotations.Index
import io.realm.annotations.LinkingObjects
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.RoomEntity
internal open class ThreadSummaryEntity(@Index var rootThreadEventId: String = "",
var rootThreadEventEntity: EventEntity? = null,
var latestThreadEventEntity: EventEntity? = null,
var rootThreadSenderName: String? = null,
var latestThreadSenderName: String? = null,
var rootThreadSenderAvatar: String? = null,
var latestThreadSenderAvatar: String? = null,
var rootThreadIsUniqueDisplayName: Boolean = false,
var isUserParticipating: Boolean = false,
var latestThreadIsUniqueDisplayName: Boolean = false,
var numberOfThreads: Int = 0
) : RealmObject() {
@LinkingObjects("threadSummaries")
val room: RealmResults<RoomEntity>? = null
companion object
}

View file

@ -0,0 +1,59 @@
/*
* 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.RealmList
import io.realm.RealmQuery
import io.realm.kotlin.createObject
import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
internal fun ThreadSummaryEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<ThreadSummaryEntity> {
return realm.where<ThreadSummaryEntity>()
.equalTo(ThreadSummaryEntityFields.ROOM.ROOM_ID, roomId)
}
internal fun ThreadSummaryEntity.Companion.where(realm: Realm, roomId: String, rootThreadEventId: String): RealmQuery<ThreadSummaryEntity> {
return where(realm, roomId)
.equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
}
internal fun ThreadSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String, rootThreadEventId: String): ThreadSummaryEntity {
return where(realm, roomId, rootThreadEventId).findFirst() ?: realm.createObject<ThreadSummaryEntity>().apply {
this.rootThreadEventId = rootThreadEventId
}
}
internal fun ThreadSummaryEntity.Companion.getOrNull(realm: Realm, roomId: String, rootThreadEventId: String): ThreadSummaryEntity? {
return where(realm, roomId, rootThreadEventId).findFirst()
}
internal fun RealmList<ThreadSummaryEntity>.find(rootThreadEventId: String): ThreadSummaryEntity? {
return this.where()
.equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
.findFirst()
}
internal fun RealmList<ThreadSummaryEntity>.findRootOrLatest(eventId: String): ThreadSummaryEntity? {
return this.where()
.beginGroup()
.equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, eventId)
.or()
.equalTo(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.EVENT_ID, eventId)
.endGroup()
.findFirst()
}

View file

@ -17,9 +17,21 @@
package org.matrix.android.sdk.internal.session.filter package org.matrix.android.sdk.internal.session.filter
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType
import timber.log.Timber
internal object FilterFactory { internal object FilterFactory {
fun createThreadsFilter(numberOfEvents: Int, userId: String?): RoomEventFilter {
Timber.i("$userId")
return RoomEventFilter(
limit = numberOfEvents,
// senders = listOf(userId),
// relationSenders = userId?.let { listOf(it) },
relationTypes = listOf(RelationType.IO_THREAD)
)
}
fun createUploadsFilter(numberOfEvents: Int): RoomEventFilter { fun createUploadsFilter(numberOfEvents: Int): RoomEventFilter {
return RoomEventFilter( return RoomEventFilter(
limit = numberOfEvents, limit = numberOfEvents,
@ -58,8 +70,8 @@ internal object FilterFactory {
private fun createElementTimelineFilter(): RoomEventFilter? { private fun createElementTimelineFilter(): RoomEventFilter? {
return null // RoomEventFilter().apply { return null // RoomEventFilter().apply {
// TODO Enable this for optimization // TODO Enable this for optimization
// types = listOfSupportedEventTypes.toMutableList() // types = listOfSupportedEventTypes.toMutableList()
// } // }
} }

View file

@ -52,12 +52,15 @@ data class RoomEventFilter(
* A list of relation types which must be exist pointing to the event being filtered. * A list of relation types which must be exist pointing to the event being filtered.
* If this list is absent then no filtering is done on relation types. * If this list is absent then no filtering is done on relation types.
*/ */
@Json(name = "relation_types") val relationTypes: List<String>? = null, // @Json(name = "relation_types") val relationTypes: List<String>? = null,
@Json(name = "io.element.relation_types") val relationTypes: List<String>? = null, // To be replaced with the above line after the release
/** /**
* A list of senders of relations which must exist pointing to the event being filtered. * A list of senders of relations which must exist pointing to the event being filtered.
* If this list is absent then no filtering is done on relation types. * If this list is absent then no filtering is done on relation types.
*/ */
@Json(name = "relation_senders") val relationSenders: List<String>? = null, // @Json(name = "relation_senders") val relationSenders: List<String>? = null,
@Json(name = "io.element.relation_senders") val relationSenders: List<String>? = null, // To be replaced with the above line after the release
/** /**
* A list of room IDs to include. If this list is absent then all rooms are included. * A list of room IDs to include. If this list is absent then all rooms are included.
*/ */

View file

@ -65,7 +65,13 @@ internal data class Capabilities(
* Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms. * Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms.
*/ */
@Json(name = "m.room_versions") @Json(name = "m.room_versions")
val roomVersions: RoomVersions? = null val roomVersions: RoomVersions? = null,
/**
* Capability to indicate if the server supports MSC3440 Threading
* True if the user can use m.thread relation, false otherwise
*/
@Json(name = "m.thread")
val threads: BooleanCapability? = null
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)

View file

@ -121,6 +121,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let { homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let {
MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it) MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it)
} }
homeServerCapabilitiesEntity.canUseThreading = capabilities?.threads?.enabled.orTrue()
} }
if (getMediaConfigResult != null) { if (getMediaConfigResult != null) {

View file

@ -36,6 +36,7 @@ import org.matrix.android.sdk.api.session.room.send.SendService
import org.matrix.android.sdk.api.session.room.state.StateService import org.matrix.android.sdk.api.session.room.state.StateService
import org.matrix.android.sdk.api.session.room.tags.TagsService import org.matrix.android.sdk.api.session.room.tags.TagsService
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.local.ThreadsLocalService
import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.timeline.TimelineService
import org.matrix.android.sdk.api.session.room.typing.TypingService import org.matrix.android.sdk.api.session.room.typing.TypingService
import org.matrix.android.sdk.api.session.room.uploads.UploadsService import org.matrix.android.sdk.api.session.room.uploads.UploadsService
@ -56,6 +57,7 @@ internal class DefaultRoom(override val roomId: String,
private val roomSummaryDataSource: RoomSummaryDataSource, private val roomSummaryDataSource: RoomSummaryDataSource,
private val timelineService: TimelineService, private val timelineService: TimelineService,
private val threadsService: ThreadsService, private val threadsService: ThreadsService,
private val threadsLocalService: ThreadsLocalService,
private val sendService: SendService, private val sendService: SendService,
private val draftService: DraftService, private val draftService: DraftService,
private val stateService: StateService, private val stateService: StateService,
@ -80,6 +82,7 @@ internal class DefaultRoom(override val roomId: String,
Room, Room,
TimelineService by timelineService, TimelineService by timelineService,
ThreadsService by threadsService, ThreadsService by threadsService,
ThreadsLocalService by threadsLocalService,
SendService by sendService, SendService by sendService,
DraftService by draftService, DraftService by draftService,
StateService by stateService, StateService by stateService,

View file

@ -197,6 +197,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
handleReaction(realm, event, roomId, isLocalEcho) handleReaction(realm, event, roomId, isLocalEcho)
} }
} }
// TODO is that ok??
// else if (event.unsignedData?.relations?.annotations != null) {
// Timber.v("###REACTION e2e Aggregation in room $roomId for event ${event.eventId}")
// handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations)
// // EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
// // ?.let {
// // TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
// // ?.forEach { tet -> tet.annotations = it }
// // }
// }
} }
EventType.REDACTION -> { EventType.REDACTION -> {
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
@ -244,7 +254,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
} }
// OPT OUT serer aggregation until API mature enough // OPT OUT serer aggregation until API mature enough
private val SHOULD_HANDLE_SERVER_AGREGGATION = false private val SHOULD_HANDLE_SERVER_AGREGGATION = false // should be true to work with e2e
private fun handleReplace(realm: Realm, private fun handleReplace(realm: Realm,
event: Event, event: Event,
@ -346,6 +356,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
/** /**
* Check if the edition is on the latest thread event, and update it accordingly * Check if the edition is on the latest thread event, and update it accordingly
* @param editedEvent The event that will be changed
* @param replaceEvent The new event
*/ */
private fun handleThreadSummaryEdition(editedEvent: EventEntity?, private fun handleThreadSummaryEdition(editedEvent: EventEntity?,
replaceEvent: TimelineEventEntity?, replaceEvent: TimelineEventEntity?,

View file

@ -86,7 +86,7 @@ internal interface RoomAPI {
suspend fun getRoomMessagesFrom(@Path("roomId") roomId: String, suspend fun getRoomMessagesFrom(@Path("roomId") roomId: String,
@Query("from") from: String, @Query("from") from: String,
@Query("dir") dir: String, @Query("dir") dir: String,
@Query("limit") limit: Int, @Query("limit") limit: Int?,
@Query("filter") filter: String? @Query("filter") filter: String?
): PaginationResponse ): PaginationResponse

View file

@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.session.room.state.SendStateTask
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
import org.matrix.android.sdk.internal.session.room.tags.DefaultTagsService import org.matrix.android.sdk.internal.session.room.tags.DefaultTagsService
import org.matrix.android.sdk.internal.session.room.threads.DefaultThreadsService import org.matrix.android.sdk.internal.session.room.threads.DefaultThreadsService
import org.matrix.android.sdk.internal.session.room.threads.local.DefaultThreadsLocalService
import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineService import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineService
import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService
import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService
@ -52,6 +53,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
private val roomSummaryDataSource: RoomSummaryDataSource, private val roomSummaryDataSource: RoomSummaryDataSource,
private val timelineServiceFactory: DefaultTimelineService.Factory, private val timelineServiceFactory: DefaultTimelineService.Factory,
private val threadsServiceFactory: DefaultThreadsService.Factory, private val threadsServiceFactory: DefaultThreadsService.Factory,
private val threadsLocalServiceFactory: DefaultThreadsLocalService.Factory,
private val sendServiceFactory: DefaultSendService.Factory, private val sendServiceFactory: DefaultSendService.Factory,
private val draftServiceFactory: DefaultDraftService.Factory, private val draftServiceFactory: DefaultDraftService.Factory,
private val stateServiceFactory: DefaultStateService.Factory, private val stateServiceFactory: DefaultStateService.Factory,
@ -79,6 +81,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
roomSummaryDataSource = roomSummaryDataSource, roomSummaryDataSource = roomSummaryDataSource,
timelineService = timelineServiceFactory.create(roomId), timelineService = timelineServiceFactory.create(roomId),
threadsService = threadsServiceFactory.create(roomId), threadsService = threadsServiceFactory.create(roomId),
threadsLocalService = threadsLocalServiceFactory.create(roomId),
sendService = sendServiceFactory.create(roomId), sendService = sendServiceFactory.create(roomId),
draftService = draftServiceFactory.create(roomId), draftService = draftServiceFactory.create(roomId),
stateService = stateServiceFactory.create(roomId), stateService = stateServiceFactory.create(roomId),

View file

@ -77,7 +77,9 @@ import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickR
import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask
import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask
import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask
import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadSummariesTask
import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
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.session.room.reporting.DefaultReportContentTask import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportContentTask
import org.matrix.android.sdk.internal.session.room.reporting.ReportContentTask import org.matrix.android.sdk.internal.session.room.reporting.ReportContentTask
@ -294,4 +296,7 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindFetchThreadTimelineTask(task: DefaultFetchThreadTimelineTask): FetchThreadTimelineTask abstract fun bindFetchThreadTimelineTask(task: DefaultFetchThreadTimelineTask): FetchThreadTimelineTask
@Binds
abstract fun bindFetchThreadSummariesTask(task: DefaultFetchThreadSummariesTask): FetchThreadSummariesTask
} }

View file

@ -37,7 +37,6 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt
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.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.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.util.fetchCopyMap import org.matrix.android.sdk.internal.util.fetchCopyMap
@ -51,7 +50,6 @@ internal class DefaultRelationService @AssistedInject constructor(
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask, private val fetchEditHistoryTask: FetchEditHistoryTask,
private val fetchThreadTimelineTask: FetchThreadTimelineTask,
private val timelineEventMapper: TimelineEventMapper, private val timelineEventMapper: TimelineEventMapper,
@SessionDatabase private val monarchy: Monarchy @SessionDatabase private val monarchy: Monarchy
) : RelationService { ) : RelationService {
@ -205,16 +203,6 @@ internal class DefaultRelationService @AssistedInject constructor(
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
} }
override suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean {
fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(
roomId,
rootThreadEventId,
null,
10
))
return true
}
/** /**
* Saves the event in database as a local echo. * Saves the event in database as a local echo.
* SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room. * SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room.

View file

@ -0,0 +1,108 @@
/*
* 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.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
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.ThreadSummaryEntity
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 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<FetchThreadSummariesTask.Params, DefaultFetchThreadSummariesTask.Result> {
data class Params(
val roomId: String,
val from: String = "",
val limit: Int = 100,
val isUserParticipating: Boolean = true
)
}
internal class DefaultFetchThreadSummariesTask @Inject constructor(
private val roomAPI: RoomAPI,
private val globalErrorReceiver: GlobalErrorReceiver,
@SessionDatabase private val monarchy: Monarchy,
private val cryptoService: DefaultCryptoService,
@UserId private val userId: String,
) : 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)
}
Timber.i("###THREADS DefaultFetchThreadSummariesTask Fetched size:${response.events.size} nextBatch:${response.end} ")
return handleResponse(response, params)
}
private suspend fun handleResponse(response: PaginationResponse,
params: FetchThreadSummariesTask.Params): Result {
val rootThreadList = response.events
monarchy.awaitTransaction { realm ->
val roomEntity = RoomEntity.where(realm, roomId = params.roomId).findFirst() ?: return@awaitTransaction
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
for (rootThreadEvent in rootThreadList) {
if (rootThreadEvent.eventId == null || rootThreadEvent.senderId == null || rootThreadEvent.type == null) {
continue
}
ThreadSummaryEntity.createOrUpdate(
threadSummaryType = ThreadSummaryUpdateType.REPLACE,
realm = realm,
roomId = params.roomId,
rootThreadEvent = rootThreadEvent,
roomMemberContentsByUser = roomMemberContentsByUser,
roomEntity = roomEntity,
userId = userId,
cryptoService = cryptoService)
}
}
return Result.SUCCESS
}
enum class Result {
SHOULD_FETCH_MORE,
REACHED_END,
SUCCESS
}
}

View file

@ -58,7 +58,6 @@ import javax.inject.Inject
/*** /***
* This class is responsible to Fetch paginated chunks of the thread timeline using the /relations API * This class is responsible to Fetch paginated chunks of the thread timeline using the /relations API
* *
*
* How it works * How it works
* *
* The problem? * The problem?

View file

@ -23,25 +23,25 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.realm.Realm import io.realm.Realm
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.timeline.TimelineEvent 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.internal.database.helper.enhanceWithEditions
import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId
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.helper.isUserParticipatingInThread import org.matrix.android.sdk.internal.database.mapper.ThreadSummaryMapper
import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
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.util.awaitTransaction import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
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, @UserId private val userId: String,
private val fetchThreadTimelineTask: FetchThreadTimelineTask,
private val fetchThreadSummariesTask: FetchThreadSummariesTask,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val timelineEventMapper: TimelineEventMapper, private val timelineEventMapper: TimelineEventMapper,
private val threadSummaryMapper: ThreadSummaryMapper
) : ThreadsService { ) : ThreadsService {
@AssistedFactory @AssistedFactory
@ -49,55 +49,40 @@ internal class DefaultThreadsService @AssistedInject constructor(
fun create(roomId: String): DefaultThreadsService fun create(roomId: String): DefaultThreadsService
} }
override fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>> { override fun getAllThreadSummariesLive(): LiveData<List<ThreadSummary>> {
return monarchy.findAllMappedWithChanges( return monarchy.findAllMappedWithChanges(
{ TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) }, { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) },
{ timelineEventMapper.map(it) } {
threadSummaryMapper.map(it)
}
) )
} }
override fun getMarkedThreadNotifications(): List<TimelineEvent> { override fun getAllThreadSummaries(): List<ThreadSummary> {
return monarchy.fetchAllMappedSync( return monarchy.fetchAllMappedSync(
{ TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) }, { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) },
{ timelineEventMapper.map(it) } { threadSummaryMapper.map(it) }
) )
} }
override fun getAllThreadsLive(): LiveData<List<TimelineEvent>> { override fun enhanceWithEditions(threads: List<ThreadSummary>): List<ThreadSummary> {
return monarchy.findAllMappedWithChanges(
{ TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
{ timelineEventMapper.map(it) }
)
}
override fun getAllThreads(): List<TimelineEvent> {
return monarchy.fetchAllMappedSync(
{ TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
{ timelineEventMapper.map(it) }
)
}
override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean {
return Realm.getInstance(monarchy.realmConfiguration).use { return Realm.getInstance(monarchy.realmConfiguration).use {
TimelineEventEntity.isUserParticipatingInThread( threads.enhanceWithEditions(it, roomId)
realm = it,
roomId = roomId,
rootThreadEventId = rootThreadEventId,
senderId = userId)
} }
} }
override fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent> { override suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int) {
return Realm.getInstance(monarchy.realmConfiguration).use { fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(
threads.mapEventsWithEdition(it, roomId) roomId = roomId,
} rootThreadEventId = rootThreadEventId,
from = from,
limit = limit
))
} }
override suspend fun markThreadAsRead(rootThreadEventId: String) { override suspend fun fetchThreadSummaries() {
monarchy.awaitTransaction { fetchThreadSummariesTask.execute(FetchThreadSummariesTask.Params(
EventEntity.where( roomId = roomId
realm = it, ))
eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
}
} }
} }

View file

@ -0,0 +1,103 @@
/*
* 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.threads.local
import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.realm.Realm
import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId
import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId
import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread
import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
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.util.awaitTransaction
internal class DefaultThreadsLocalService @AssistedInject constructor(
@Assisted private val roomId: String,
@UserId private val userId: String,
@SessionDatabase private val monarchy: Monarchy,
private val timelineEventMapper: TimelineEventMapper,
) : ThreadsLocalService {
@AssistedFactory
interface Factory {
fun create(roomId: String): DefaultThreadsLocalService
}
override fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>> {
return monarchy.findAllMappedWithChanges(
{ TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
{ timelineEventMapper.map(it) }
)
}
override fun getMarkedThreadNotifications(): List<TimelineEvent> {
return monarchy.fetchAllMappedSync(
{ TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
{ timelineEventMapper.map(it) }
)
}
override fun getAllThreadsLive(): LiveData<List<TimelineEvent>> {
return monarchy.findAllMappedWithChanges(
{ TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
{ timelineEventMapper.map(it) }
)
}
override fun getAllThreads(): List<TimelineEvent> {
return monarchy.fetchAllMappedSync(
{ TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
{ timelineEventMapper.map(it) }
)
}
override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean {
return Realm.getInstance(monarchy.realmConfiguration).use {
TimelineEventEntity.isUserParticipatingInThread(
realm = it,
roomId = roomId,
rootThreadEventId = rootThreadEventId,
senderId = userId)
}
}
override fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent> {
return Realm.getInstance(monarchy.realmConfiguration).use {
threads.mapEventsWithEdition(it, roomId)
}
}
override suspend fun markThreadAsRead(rootThreadEventId: String) {
monarchy.awaitTransaction {
EventEntity.where(
realm = it,
eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
}
}
}

View file

@ -23,10 +23,12 @@ import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
import org.matrix.android.sdk.api.session.initsync.InitSyncStep import org.matrix.android.sdk.api.session.initsync.InitSyncStep
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
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.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
import org.matrix.android.sdk.api.session.sync.model.InvitedRoomSync import org.matrix.android.sdk.api.session.sync.model.InvitedRoomSync
import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral
import org.matrix.android.sdk.api.session.sync.model.RoomSync import org.matrix.android.sdk.api.session.sync.model.RoomSync
@ -36,6 +38,7 @@ import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.database.helper.addIfNecessary import org.matrix.android.sdk.internal.database.helper.addIfNecessary
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
import org.matrix.android.sdk.internal.database.helper.createOrUpdate
import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.asDomain
@ -48,6 +51,7 @@ import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
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.deleteOnCascade import org.matrix.android.sdk.internal.database.model.deleteOnCascade
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
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.find import org.matrix.android.sdk.internal.database.query.find
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
@ -60,6 +64,7 @@ import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.extensions.clearWith import org.matrix.android.sdk.internal.extensions.clearWith
import org.matrix.android.sdk.internal.session.StreamEventsManager import org.matrix.android.sdk.internal.session.StreamEventsManager
import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService
import org.matrix.android.sdk.internal.session.initsync.ProgressReporter import org.matrix.android.sdk.internal.session.initsync.ProgressReporter
import org.matrix.android.sdk.internal.session.initsync.mapWithProgress import org.matrix.android.sdk.internal.session.initsync.mapWithProgress
import org.matrix.android.sdk.internal.session.initsync.reportSubtask import org.matrix.android.sdk.internal.session.initsync.reportSubtask
@ -86,6 +91,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private val threadsAwarenessHandler: ThreadsAwarenessHandler, private val threadsAwarenessHandler: ThreadsAwarenessHandler,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
@UserId private val userId: String, @UserId private val userId: String,
private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
private val lightweightSettingsStorage: LightweightSettingsStorage, private val lightweightSettingsStorage: LightweightSettingsStorage,
private val timelineInput: TimelineInput, private val timelineInput: TimelineInput,
private val liveEventService: Lazy<StreamEventsManager>) { private val liveEventService: Lazy<StreamEventsManager>) {
@ -345,7 +351,6 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
return roomEntity return roomEntity
} }
val customList = arrayListOf<String>()
private fun handleTimelineEvents(realm: Realm, private fun handleTimelineEvents(realm: Realm,
roomId: String, roomId: String,
roomEntity: RoomEntity, roomEntity: RoomEntity,
@ -420,6 +425,17 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
optimizedThreadSummaryMap[it] = eventEntity optimizedThreadSummaryMap[it] = eventEntity
// Add the same thread timeline event to Thread Chunk // Add the same thread timeline event to Thread Chunk
addToThreadChunkIfNeeded(realm, roomId, it, timelineEventAdded, roomEntity) addToThreadChunkIfNeeded(realm, roomId, it, timelineEventAdded, roomEntity)
// Add thread list if needed
if(homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreading) {
ThreadSummaryEntity.createOrUpdate(
threadSummaryType = ThreadSummaryUpdateType.ADD,
realm = realm,
roomId = roomId,
threadEventEntity = eventEntity,
roomMemberContentsByUser = roomMemberContentsByUser,
userId = userId,
roomEntity = roomEntity)
}
} ?: run { } ?: run {
// This is a normal event or a root thread one // This is a normal event or a root thread one
optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity

View file

@ -32,7 +32,6 @@ import im.vector.app.features.home.room.detail.arguments.TimelineArgs
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.views.ThreadListFragment import im.vector.app.features.home.room.threads.list.views.ThreadListFragment
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -92,14 +91,7 @@ class ThreadsActivity : VectorBaseActivity<ActivityThreadsBinding>() {
* This function is used to navigate to the selected thread timeline. * This function is used to navigate to the selected thread timeline.
* One usage of that is from the Threads Activity * One usage of that is from the Threads Activity
*/ */
fun navigateToThreadTimeline( fun navigateToThreadTimeline(threadTimelineArgs: ThreadTimelineArgs) {
timelineEvent: TimelineEvent) {
val roomThreadDetailArgs = ThreadTimelineArgs(
roomId = timelineEvent.roomId,
displayName = timelineEvent.senderInfo.displayName,
avatarUrl = timelineEvent.senderInfo.avatarUrl,
roomEncryptionTrustLevel = null,
rootThreadEventId = timelineEvent.eventId)
val commonOption: (FragmentTransaction) -> Unit = { val commonOption: (FragmentTransaction) -> Unit = {
it.setCustomAnimations( it.setCustomAnimations(
R.anim.animation_slide_in_right, R.anim.animation_slide_in_right,
@ -111,8 +103,8 @@ class ThreadsActivity : VectorBaseActivity<ActivityThreadsBinding>() {
container = views.threadsActivityFragmentContainer, container = views.threadsActivityFragmentContainer,
fragmentClass = TimelineFragment::class.java, fragmentClass = TimelineFragment::class.java,
params = TimelineArgs( params = TimelineArgs(
roomId = timelineEvent.roomId, roomId = threadTimelineArgs.roomId,
threadTimelineArgs = roomThreadDetailArgs threadTimelineArgs = threadTimelineArgs
), ),
option = commonOption option = commonOption
) )

View file

@ -23,15 +23,19 @@ import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.resources.StringProvider 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.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 stringProvider: StringProvider,
private val dateFormatter: VectorDateFormatter private val dateFormatter: VectorDateFormatter,
private val session: Session
) : EpoxyController() { ) : EpoxyController() {
var listener: Listener? = null var listener: Listener? = null
@ -43,10 +47,59 @@ class ThreadListController @Inject constructor(
requestModelBuild() requestModelBuild()
} }
override fun buildModels() { override fun buildModels() =
when (session.getHomeServerCapabilities().canUseThreading) {
true -> buildThreadSummaries()
false -> buildThreadList()
}
/**
* Building thread summaries when homeserver
* supports threading
*/
private fun buildThreadSummaries() {
val safeViewState = viewState ?: return val safeViewState = viewState ?: return
val host = this 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 decryptionErrorMessage = stringProvider.getString(R.string.encrypted_message)
val rootThreadEdition = threadSummary.threadEditions.rootThreadEdition
val latestThreadEdition = threadSummary.threadEditions.latestThreadEdition
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(rootThreadEdition ?: threadSummary.rootEvent?.getDecryptedTextSummary() ?: decryptionErrorMessage)
lastMessage(latestThreadEdition ?: threadSummary.latestEvent?.getDecryptedTextSummary() ?: decryptionErrorMessage)
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 host = this
safeViewState.rootThreadEventList.invoke() safeViewState.rootThreadEventList.invoke()
?.filter { ?.filter {
if (safeViewState.shouldFilterThreads) { if (safeViewState.shouldFilterThreads) {
@ -74,13 +127,14 @@ class ThreadListController @Inject constructor(
lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString()) lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString())
lastMessageMatrixItem(timelineEvent.root.threadDetails?.threadSummarySenderInfo?.toMatrixItem()) lastMessageMatrixItem(timelineEvent.root.threadDetails?.threadSummarySenderInfo?.toMatrixItem())
itemClickListener { itemClickListener {
host.listener?.onThreadClicked(timelineEvent) host.listener?.onThreadListClicked(timelineEvent)
} }
} }
} }
} }
interface Listener { interface Listener {
fun onThreadClicked(timelineEvent: TimelineEvent) fun onThreadSummaryClicked(threadSummary: ThreadSummary)
fun onThreadListClicked(timelineEvent: TimelineEvent)
} }
} }

View file

@ -28,6 +28,7 @@ import im.vector.app.core.platform.VectorViewModel
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.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
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.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
@ -53,11 +54,41 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState
} }
init { init {
observeThreadsList() observeThreads()
fetchThreadList()
} }
override fun handle(action: EmptyAction) {} override fun handle(action: EmptyAction) {}
/**
* Observing thread list with respect to homeserver
* capabilities
*/
private fun observeThreads() {
when (session.getHomeServerCapabilities().canUseThreading) {
true -> observeThreadSummaries()
false -> observeThreadsList()
}
}
/**
* Observing thread summaries when homeserver support
* threading
*/
private fun observeThreadSummaries() {
room?.flow()
?.liveThreadSummaries()
?.map { room.enhanceWithEditions(it) }
?.flowOn(room.coroutineDispatchers.io)
?.execute { asyncThreads ->
copy(threadSummaryList = asyncThreads)
}
}
/**
* Observing thread list when homeserver do not support
* threading
*/
private fun observeThreadsList() { private fun observeThreadsList() {
room?.flow() room?.flow()
?.liveThreadList() ?.liveThreadList()
@ -74,6 +105,14 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState
} }
} }
private fun fetchThreadList() {
viewModelScope.launch {
room?.fetchThreadSummaries()
}
}
fun canHomeserverUseThreading() = session.getHomeServerCapabilities().canUseThreading
fun applyFiltering(shouldFilterThreads: Boolean) { fun applyFiltering(shouldFilterThreads: Boolean) {
setState { setState {
copy(shouldFilterThreads = shouldFilterThreads) copy(shouldFilterThreads = shouldFilterThreads)

View file

@ -20,13 +20,14 @@ 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 roomId: String val roomId: String
) : MavericksState { ) : MavericksState {
constructor(args: ThreadListArgs) : this(roomId = args.roomId) constructor(args: ThreadListArgs) : this(roomId = args.roomId)
} }

View file

@ -34,9 +34,11 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.animation.TimelineItemAnimator import im.vector.app.features.home.room.detail.timeline.animation.TimelineItemAnimator
import im.vector.app.features.home.room.threads.ThreadsActivity 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.list.viewmodel.ThreadListController import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListController
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 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.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject import javax.inject.Inject
@ -111,12 +113,30 @@ class ThreadListFragment @Inject constructor(
views.includeThreadListToolbar.roomToolbarThreadSubtitleTextView.text = threadListArgs.displayName views.includeThreadListToolbar.roomToolbarThreadSubtitleTextView.text = threadListArgs.displayName
} }
override fun onThreadClicked(timelineEvent: TimelineEvent) { override fun onThreadSummaryClicked(threadSummary: ThreadSummary) {
(activity as? ThreadsActivity)?.navigateToThreadTimeline(timelineEvent) val roomThreadDetailArgs = ThreadTimelineArgs(
roomId = threadSummary.roomId,
displayName = threadSummary.rootThreadSenderInfo.displayName,
avatarUrl = threadSummary.rootThreadSenderInfo.avatarUrl,
roomEncryptionTrustLevel = null,
rootThreadEventId = threadSummary.rootEventId)
(activity as? ThreadsActivity)?.navigateToThreadTimeline(roomThreadDetailArgs)
}
override fun onThreadListClicked(timelineEvent: TimelineEvent) {
val threadTimelineArgs = ThreadTimelineArgs(
roomId = timelineEvent.roomId,
displayName = timelineEvent.senderInfo.displayName,
avatarUrl = timelineEvent.senderInfo.avatarUrl,
roomEncryptionTrustLevel = null,
rootThreadEventId = timelineEvent.eventId)
(activity as? ThreadsActivity)?.navigateToThreadTimeline(threadTimelineArgs)
} }
private fun renderEmptyStateIfNeeded(state: ThreadListViewState) { private fun renderEmptyStateIfNeeded(state: ThreadListViewState) {
val show = state.rootThreadEventList.invoke().isNullOrEmpty() when (threadListViewModel.canHomeserverUseThreading()) {
views.threadListEmptyConstraintLayout.isVisible = show true -> views.threadListEmptyConstraintLayout.isVisible = state.threadSummaryList.invoke().isNullOrEmpty()
false -> views.threadListEmptyConstraintLayout.isVisible = state.rootThreadEventList.invoke().isNullOrEmpty()
}
} }
} }