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.notification.RoomNotificationState
import org.matrix.android.sdk.api.session.room.send.UserDraft
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional
@ -101,13 +102,18 @@ class FlowRoom(private val room: Room) {
return room.getLiveRoomNotificationState().asFlow()
}
fun liveThreadSummaries(): Flow<List<ThreadSummary>> {
return room.getAllThreadSummariesLive().asFlow()
.startWith(room.coroutineDispatchers.io) {
room.getAllThreadSummaries()
}
}
fun liveThreadList(): Flow<List<ThreadRootEvent>> {
return room.getAllThreadsLive().asFlow()
.startWith(room.coroutineDispatchers.io) {
room.getAllThreads()
}
}
fun liveLocalUnreadThreadList(): Flow<List<ThreadRootEvent>> {
return room.getMarkedThreadNotificationsLive().asFlow()
.startWith(room.coroutineDispatchers.io) {

View File

@ -49,5 +49,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class AggregatedRelations(
@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.
* 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 {

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

View File

@ -163,13 +163,4 @@ interface RelationService {
autoMarkdown: Boolean = false,
formattedText: String? = null,
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
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.
* It's implemented at the room level within the main timeline.
* This interface defines methods to interact with thread related features.
* 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 {
/**
* 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
*/
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
* Enhance the provided ThreadSummary[List] by adding the latest
* message edition for that thread
* @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.
* note: read receipts within threads are not yet supported with the API
* @param rootThreadEventId the root eventId of the current thread
* Fetch all thread replies for the specified thread using the /relations api
* @param rootThreadEventId the root thread eventId
* @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
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.room.model.RoomMemberSummary
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.toMatrixItemOrNull() = tryOrNull { MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl) }
fun SpaceChildInfo.toMatrixItem() = if (roomType == RoomType.SPACE) {
MatrixItem.SpaceItem(childRoomId, name ?: canonicalAlias, avatarUrl)
} 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.MigrateSessionTo025
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 timber.log.Timber
import javax.inject.Inject
@ -58,7 +59,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
override fun equals(other: Any?) = other is RealmSessionStoreMigration
override fun hashCode() = 1000
val schemaVersion = 25L
val schemaVersion = 27L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
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 < 25) MigrateSessionTo025(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
}
private fun computeIsUnique(
fun computeIsUnique(
realm: Realm,
roomId: String,
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.RoomEntity
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
internal fun RoomEntity.addIfNecessary(chunkEntity: ChunkEntity) {
if (!chunks.contains(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.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
@ -93,7 +93,7 @@ internal fun EventEntity.markEventAsRoot(
* @param rootThreadEventId The root eventId that will find the number of threads
* @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
val messages = TimelineEventEntity
.whereRoomId(realm, roomId = roomId)
@ -124,7 +124,7 @@ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId:
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,
lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
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
}
internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRoot: Boolean) {
internal fun ChunkEntity.deleteOnCascade(
deleteStateEvents: Boolean,
canDeleteRoot: Boolean) {
assertIsManaged()
if (deleteStateEvents) {
stateEvents.deleteAllFromRealm()
}
timelineEvents.clearWith {
val deleteRoot = canDeleteRoot && (it.root?.stateKey == null || deleteStateEvents)
if (deleteRoot) {
room?.firstOrNull()?.removeThreadSummaryIfNeeded(it.eventId)
}
it.deleteOnCascade(deleteRoot)
}
deleteFromRealm()

View File

@ -34,14 +34,14 @@ internal open class EventEntity(@Index var eventId: String = "",
@Index var stateKey: String? = null,
var originServerTs: Long? = null,
@Index var sender: String? = null,
// Can contain a serialized MatrixError
// Can contain a serialized MatrixError
var sendStateDetails: String? = null,
var age: Long? = 0,
var unsignedData: String? = null,
var redacts: String? = null,
var decryptionResultJson: String? = 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 rootThreadEventId: String? = null,
var numberOfThreads: Int = 0,

View File

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

View File

@ -20,10 +20,14 @@ import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
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 = "",
var chunks: RealmList<ChunkEntity> = RealmList(),
var sendingTimelineEvents: RealmList<TimelineEventEntity> = RealmList(),
var threadSummaries: RealmList<ThreadSummaryEntity> = RealmList(),
var accountData: RealmList<RoomAccountDataEntity> = RealmList()
) : RealmObject() {
@ -46,3 +50,10 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "",
}
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 org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
/**
* Realm module for Session
@ -66,6 +67,7 @@ import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntit
RoomAccountDataEntity::class,
SpaceChildSummaryEntity::class,
SpaceParentSummaryEntity::class,
UserPresenceEntity::class
UserPresenceEntity::class,
ThreadSummaryEntity::class
])
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
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 {
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 {
return RoomEventFilter(
limit = numberOfEvents,
@ -58,8 +70,8 @@ internal object FilterFactory {
private fun createElementTimelineFilter(): RoomEventFilter? {
return null // RoomEventFilter().apply {
// TODO Enable this for optimization
// types = listOfSupportedEventTypes.toMutableList()
// TODO Enable this for optimization
// 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.
* 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.
* 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.
*/

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.
*/
@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)

View File

@ -121,6 +121,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let {
MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it)
}
homeServerCapabilitiesEntity.canUseThreading = capabilities?.threads?.enabled.orTrue()
}
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.tags.TagsService
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.typing.TypingService
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 timelineService: TimelineService,
private val threadsService: ThreadsService,
private val threadsLocalService: ThreadsLocalService,
private val sendService: SendService,
private val draftService: DraftService,
private val stateService: StateService,
@ -80,6 +82,7 @@ internal class DefaultRoom(override val roomId: String,
Room,
TimelineService by timelineService,
ThreadsService by threadsService,
ThreadsLocalService by threadsLocalService,
SendService by sendService,
DraftService by draftService,
StateService by stateService,

View File

@ -197,6 +197,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
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 -> {
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
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,
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
* @param editedEvent The event that will be changed
* @param replaceEvent The new event
*/
private fun handleThreadSummaryEdition(editedEvent: EventEntity?,
replaceEvent: TimelineEventEntity?,

View File

@ -86,7 +86,7 @@ internal interface RoomAPI {
suspend fun getRoomMessagesFrom(@Path("roomId") roomId: String,
@Query("from") from: String,
@Query("dir") dir: String,
@Query("limit") limit: Int,
@Query("limit") limit: Int?,
@Query("filter") filter: String?
): 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.tags.DefaultTagsService
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.typing.DefaultTypingService
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 timelineServiceFactory: DefaultTimelineService.Factory,
private val threadsServiceFactory: DefaultThreadsService.Factory,
private val threadsLocalServiceFactory: DefaultThreadsLocalService.Factory,
private val sendServiceFactory: DefaultSendService.Factory,
private val draftServiceFactory: DefaultDraftService.Factory,
private val stateServiceFactory: DefaultStateService.Factory,
@ -79,6 +81,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
roomSummaryDataSource = roomSummaryDataSource,
timelineService = timelineServiceFactory.create(roomId),
threadsService = threadsServiceFactory.create(roomId),
threadsLocalService = threadsLocalServiceFactory.create(roomId),
sendService = sendServiceFactory.create(roomId),
draftService = draftServiceFactory.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.FindReactionEventForUndoTask
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.FetchThreadSummariesTask
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.ReportContentTask
@ -294,4 +296,7 @@ internal abstract class RoomModule {
@Binds
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.query.where
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.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.util.fetchCopyMap
@ -51,7 +50,6 @@ internal class DefaultRelationService @AssistedInject constructor(
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask,
private val fetchThreadTimelineTask: FetchThreadTimelineTask,
private val timelineEventMapper: TimelineEventMapper,
@SessionDatabase private val monarchy: Monarchy
) : RelationService {
@ -205,16 +203,6 @@ internal class DefaultRelationService @AssistedInject constructor(
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.
* 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
*
*
* How it works
*
* The problem?

View File

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

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.EventType
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.room.model.Membership
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.ThreadSummaryUpdateType
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.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.database.helper.addIfNecessary
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.lightweight.LightweightSettingsStorage
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.TimelineEventEntity
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.find
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.session.StreamEventsManager
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.mapWithProgress
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 roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
@UserId private val userId: String,
private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
private val lightweightSettingsStorage: LightweightSettingsStorage,
private val timelineInput: TimelineInput,
private val liveEventService: Lazy<StreamEventsManager>) {
@ -345,7 +351,6 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
return roomEntity
}
val customList = arrayListOf<String>()
private fun handleTimelineEvents(realm: Realm,
roomId: String,
roomEntity: RoomEntity,
@ -420,6 +425,17 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
optimizedThreadSummaryMap[it] = eventEntity
// Add the same thread timeline event to Thread Chunk
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 {
// This is a normal event or a root thread one
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.ThreadTimelineArgs
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
@AndroidEntryPoint
@ -92,14 +91,7 @@ class ThreadsActivity : VectorBaseActivity<ActivityThreadsBinding>() {
* This function is used to navigate to the selected thread timeline.
* One usage of that is from the Threads Activity
*/
fun navigateToThreadTimeline(
timelineEvent: TimelineEvent) {
val roomThreadDetailArgs = ThreadTimelineArgs(
roomId = timelineEvent.roomId,
displayName = timelineEvent.senderInfo.displayName,
avatarUrl = timelineEvent.senderInfo.avatarUrl,
roomEncryptionTrustLevel = null,
rootThreadEventId = timelineEvent.eventId)
fun navigateToThreadTimeline(threadTimelineArgs: ThreadTimelineArgs) {
val commonOption: (FragmentTransaction) -> Unit = {
it.setCustomAnimations(
R.anim.animation_slide_in_right,
@ -111,8 +103,8 @@ class ThreadsActivity : VectorBaseActivity<ActivityThreadsBinding>() {
container = views.threadsActivityFragmentContainer,
fragmentClass = TimelineFragment::class.java,
params = TimelineArgs(
roomId = timelineEvent.roomId,
threadTimelineArgs = roomThreadDetailArgs
roomId = threadTimelineArgs.roomId,
threadTimelineArgs = threadTimelineArgs
),
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.features.home.AvatarRenderer
import im.vector.app.features.home.room.threads.list.model.threadListItem
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.api.util.toMatrixItemOrNull
import javax.inject.Inject
class ThreadListController @Inject constructor(
private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider,
private val dateFormatter: VectorDateFormatter
private val dateFormatter: VectorDateFormatter,
private val session: Session
) : EpoxyController() {
var listener: Listener? = null
@ -43,10 +47,59 @@ class ThreadListController @Inject constructor(
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 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()
?.filter {
if (safeViewState.shouldFilterThreads) {
@ -74,13 +127,14 @@ class ThreadListController @Inject constructor(
lastMessageCounter(timelineEvent.root.threadDetails?.numberOfThreads.toString())
lastMessageMatrixItem(timelineEvent.root.threadDetails?.threadSummarySenderInfo?.toMatrixItem())
itemClickListener {
host.listener?.onThreadClicked(timelineEvent)
host.listener?.onThreadListClicked(timelineEvent)
}
}
}
}
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 kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent
import org.matrix.android.sdk.flow.flow
@ -53,11 +54,41 @@ class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState
}
init {
observeThreadsList()
observeThreads()
fetchThreadList()
}
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() {
room?.flow()
?.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) {
setState {
copy(shouldFilterThreads = shouldFilterThreads)

View File

@ -20,13 +20,14 @@ import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.home.room.threads.arguments.ThreadListArgs
import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
import org.matrix.android.sdk.api.session.threads.ThreadTimelineEvent
data class ThreadListViewState(
val threadSummaryList: Async<List<ThreadSummary>> = Uninitialized,
val rootThreadEventList: Async<List<ThreadTimelineEvent>> = Uninitialized,
val shouldFilterThreads: Boolean = false,
val roomId: String
) : MavericksState {
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.threads.ThreadsActivity
import im.vector.app.features.home.room.threads.arguments.ThreadListArgs
import im.vector.app.features.home.room.threads.arguments.ThreadTimelineArgs
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListController
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewModel
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.util.MatrixItem
import javax.inject.Inject
@ -111,12 +113,30 @@ class ThreadListFragment @Inject constructor(
views.includeThreadListToolbar.roomToolbarThreadSubtitleTextView.text = threadListArgs.displayName
}
override fun onThreadClicked(timelineEvent: TimelineEvent) {
(activity as? ThreadsActivity)?.navigateToThreadTimeline(timelineEvent)
override fun onThreadSummaryClicked(threadSummary: ThreadSummary) {
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) {
val show = state.rootThreadEventList.invoke().isNullOrEmpty()
views.threadListEmptyConstraintLayout.isVisible = show
when (threadListViewModel.canHomeserverUseThreading()) {
true -> views.threadListEmptyConstraintLayout.isVisible = state.threadSummaryList.invoke().isNullOrEmpty()
false -> views.threadListEmptyConstraintLayout.isVisible = state.rootThreadEventList.invoke().isNullOrEmpty()
}
}
}