- Thread Summary along with optimization

- Create new thread & reply to thread
This commit is contained in:
ariskotsomitopoulos 2021-11-15 19:17:13 +02:00
parent ecc9b59ad1
commit 8c539426e6
40 changed files with 481 additions and 264 deletions

View File

@ -147,6 +147,11 @@ project(":diff-match-patch") {
} }
} }
// Global configurations across all modules
ext {
isThreadingEnabled = true
}
//project(":matrix-sdk-android") { //project(":matrix-sdk-android") {
// sonarqube { // sonarqube {
// properties { // properties {

View File

@ -38,6 +38,8 @@ android {
resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\"" resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\"" resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\""
// Indicates whether or not threading support is enabled
buildConfigField "Boolean", "THREADING_ENABLED", "${isThreadingEnabled}"
defaultConfig { defaultConfig {
consumerProguardFiles 'proguard-rules.pro' consumerProguardFiles 'proguard-rules.pro'
} }

View File

@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
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.threads.ThreadDetails
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
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.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
@ -97,7 +98,7 @@ data class Event(
var sendStateDetails: String? = null var sendStateDetails: String? = null
@Transient @Transient
var isRootThread: Boolean = false var threadDetails: ThreadDetails? = null
fun sendStateError(): MatrixError? { fun sendStateError(): MatrixError? {
return sendStateDetails?.let { return sendStateDetails?.let {
@ -124,6 +125,7 @@ data class Event(
it.mCryptoErrorReason = mCryptoErrorReason it.mCryptoErrorReason = mCryptoErrorReason
it.sendState = sendState it.sendState = sendState
it.ageLocalTs = ageLocalTs it.ageLocalTs = ageLocalTs
it.threadDetails = threadDetails
} }
} }
@ -186,6 +188,16 @@ data class Event(
return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) } return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) }
} }
fun getDecryptedMessageText(): String {
return getValueFromPayload(mxDecryptionResult?.payload).orEmpty()
}
@Suppress("UNCHECKED_CAST")
private fun getValueFromPayload(payload: JsonDict?, key: String = "body"): String? {
val content = payload?.get("content") as? JsonDict
return content?.get(key) as? String
}
/** /**
* Tells if the event is redacted * Tells if the event is redacted
*/ */
@ -218,7 +230,7 @@ data class Event(
if (mCryptoError != other.mCryptoError) return false if (mCryptoError != other.mCryptoError) return false
if (mCryptoErrorReason != other.mCryptoErrorReason) return false if (mCryptoErrorReason != other.mCryptoErrorReason) return false
if (sendState != other.sendState) return false if (sendState != other.sendState) return false
if (threadDetails != other.threadDetails) return false
return true return true
} }
@ -237,6 +249,8 @@ data class Event(
result = 31 * result + (mCryptoError?.hashCode() ?: 0) result = 31 * result + (mCryptoError?.hashCode() ?: 0)
result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0) result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0)
result = 31 * result + sendState.hashCode() result = 31 * result + sendState.hashCode()
result = 31 * result + threadDetails.hashCode()
return result return result
} }
} }

View File

@ -43,7 +43,7 @@ interface Timeline {
/** /**
* This must be called before any other method after creating the timeline. It ensures the underlying database is open * This must be called before any other method after creating the timeline. It ensures the underlying database is open
*/ */
fun start() fun start(rootThreadEventId: String? = null)
/** /**
* This must be called when you don't need the timeline. It ensures the underlying database get closed. * This must be called when you don't need the timeline. It ensures the underlying database get closed.

View File

@ -0,0 +1,26 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.threads
import org.matrix.android.sdk.api.session.room.sender.SenderInfo
data class ThreadDetails(
val isRootThread: Boolean = false,
val numberOfThreads: Int = 0,
val threadSummarySenderInfo: SenderInfo? = null,
val threadSummaryLatestTextMessage: String? = null
)

View File

@ -369,10 +369,12 @@ internal object RealmSessionStoreMigration : RealmMigration {
private fun migrateTo19(realm: DynamicRealm) { private fun migrateTo19(realm: DynamicRealm) {
Timber.d("Step 18 -> 19") Timber.d("Step 18 -> 19")
val eventEntity = realm.schema.get("TimelineEventEntity") ?: return
realm.schema.get("EventEntity") realm.schema.get("EventEntity")
?.addField(EventEntityFields.IS_THREAD, Boolean::class.java, FieldAttribute.INDEXED) ?.addField(EventEntityFields.IS_ROOT_THREAD, Boolean::class.java, FieldAttribute.INDEXED)
?.addField(EventEntityFields.ROOT_THREAD_EVENT_ID, String::class.java) ?.addField(EventEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
realm.schema.get("ChunkEntity") ?.addField(EventEntityFields.NUMBER_OF_THREADS, Int::class.java)
?.addField(ChunkEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED) ?.addRealmObjectField(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.`$`, eventEntity)
} }
} }

View File

@ -0,0 +1,88 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.helper
import io.realm.Realm
import io.realm.RealmResults
import io.realm.Sort
import org.matrix.android.sdk.BuildConfig
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.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId
/**
* Finds the root thread event and update it with the latest message summary along with the number
* of threads included. If there is no root thread event no action is done
*/
internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded() {
if (!BuildConfig.THREADING_ENABLED) return
for ((rootThreadEventId, eventEntity) in this) {
eventEntity.findAllThreadsForRootEventId(eventEntity.realm, rootThreadEventId).let {
if (it.isNullOrEmpty()) return@let
val latestMessage = it.firstOrNull()
// If this is a thread message, find its root event if exists
val rootThreadEvent = if (eventEntity.isThread()) eventEntity.findRootThreadEvent() else eventEntity
rootThreadEvent?.markEventAsRoot(
threadsCounted = it.size,
latestMessageTimelineEventEntity = latestMessage
)
}
}
}
/**
* Finds the root event of the the current thread event message.
* Returns the EventEntity or null if the root event do not exist
*/
internal fun EventEntity.findRootThreadEvent(): EventEntity? =
rootThreadEventId?.let {
EventEntity
.where(realm, it)
.findFirst()
}
/**
* Mark or update the current event a root thread event
*/
internal fun EventEntity.markEventAsRoot(threadsCounted: Int,
latestMessageTimelineEventEntity: TimelineEventEntity?) {
isRootThread = true
numberOfThreads = threadsCounted
threadSummaryLatestMessage = latestMessageTimelineEventEntity
}
/**
* Find all TimelineEventEntity that are threads bind to the Event with rootThreadEventId
* @param rootThreadEventId The root eventId that will try to find bind threads
*/
internal fun EventEntity.findAllThreadsForRootEventId(realm: Realm, rootThreadEventId: String): RealmResults<TimelineEventEntity> =
TimelineEventEntity
.whereRoomId(realm, roomId = roomId)
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()

View File

@ -24,6 +24,9 @@ import org.matrix.android.sdk.api.session.events.model.UnsignedData
import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
import org.matrix.android.sdk.api.session.events.model.isThread import org.matrix.android.sdk.api.session.events.model.isThread
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.sender.SenderInfo
import org.matrix.android.sdk.api.session.threads.ThreadDetails
import org.matrix.android.sdk.api.util.JsonDict
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.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
@ -41,8 +44,6 @@ internal object EventMapper {
eventEntity.isUseless = IsUselessResolver.isUseless(event) eventEntity.isUseless = IsUselessResolver.isUseless(event)
eventEntity.stateKey = event.stateKey eventEntity.stateKey = event.stateKey
eventEntity.type = event.type ?: EventType.MISSING_TYPE eventEntity.type = event.type ?: EventType.MISSING_TYPE
eventEntity.isThread = if(event.isRootThread) true else event.isThread()
eventEntity.rootThreadEventId = if(event.isRootThread) null else event.getRootThreadEventId()
eventEntity.sender = event.senderId eventEntity.sender = event.senderId
eventEntity.originServerTs = event.originServerTs eventEntity.originServerTs = event.originServerTs
eventEntity.redacts = event.redacts eventEntity.redacts = event.redacts
@ -55,6 +56,9 @@ internal object EventMapper {
} }
eventEntity.decryptionErrorReason = event.mCryptoErrorReason eventEntity.decryptionErrorReason = event.mCryptoErrorReason
eventEntity.decryptionErrorCode = event.mCryptoError?.name eventEntity.decryptionErrorCode = event.mCryptoError?.name
eventEntity.isRootThread = event.threadDetails?.isRootThread ?: false
eventEntity.rootThreadEventId = event.getRootThreadEventId()
eventEntity.numberOfThreads = event.threadDetails?.numberOfThreads ?: 0
return eventEntity return eventEntity
} }
@ -97,7 +101,20 @@ internal object EventMapper {
MXCryptoError.ErrorType.valueOf(errorCode) MXCryptoError.ErrorType.valueOf(errorCode)
} }
it.mCryptoErrorReason = eventEntity.decryptionErrorReason it.mCryptoErrorReason = eventEntity.decryptionErrorReason
it.isRootThread = eventEntity.isRootThread()
it.threadDetails = ThreadDetails(
isRootThread = eventEntity.isRootThread,
numberOfThreads = eventEntity.numberOfThreads,
threadSummarySenderInfo = eventEntity.threadSummaryLatestMessage?.let { timelineEventEntity ->
SenderInfo(
userId = timelineEventEntity.root?.sender ?: "",
displayName = timelineEventEntity.senderName,
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
avatarUrl = timelineEventEntity.senderAvatar
)
},
threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedMessageText().orEmpty()
)
} }
} }
} }

View File

@ -27,7 +27,6 @@ import org.matrix.android.sdk.internal.extensions.clearWith
internal open class ChunkEntity(@Index var prevToken: String? = null, internal open class ChunkEntity(@Index var prevToken: String? = null,
// Because of gaps we can have several chunks with nextToken == null // Because of gaps we can have several chunks with nextToken == null
@Index var nextToken: String? = null, @Index var nextToken: String? = null,
@Index var rootThreadEventId: String? = null,
var stateEvents: RealmList<EventEntity> = RealmList(), var stateEvents: RealmList<EventEntity> = RealmList(),
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(), var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
var numberOfTimelineEvents: Long = 0, var numberOfTimelineEvents: Long = 0,
@ -46,7 +45,6 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
companion object companion object
fun isThreadChunk() = rootThreadEventId != null
} }
internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRoot: Boolean) { internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRoot: Boolean) {

View File

@ -27,8 +27,6 @@ import org.matrix.android.sdk.internal.extensions.assertIsManaged
internal open class EventEntity(@Index var eventId: String = "", internal open class EventEntity(@Index var eventId: String = "",
@Index var roomId: String = "", @Index var roomId: String = "",
@Index var type: String = "", @Index var type: String = "",
@Index var isThread: Boolean = false,
var rootThreadEventId: String? = null,
var content: String? = null, var content: String? = null,
var prevContent: String? = null, var prevContent: String? = null,
var isUseless: Boolean = false, var isUseless: Boolean = false,
@ -43,7 +41,13 @@ internal open class EventEntity(@Index var eventId: String = "",
var decryptionResultJson: String? = null, var decryptionResultJson: String? = null,
var decryptionErrorCode: String? = null, var decryptionErrorCode: String? = null,
var decryptionErrorReason: String? = null, var decryptionErrorReason: String? = null,
var ageLocalTs: Long? = null var ageLocalTs: Long? = null,
// 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,
var threadSummaryLatestMessage: TimelineEventEntity? = null
) : RealmObject() { ) : RealmObject() {
private var sendStateStr: String = SendState.UNKNOWN.name private var sendStateStr: String = SendState.UNKNOWN.name
@ -78,9 +82,6 @@ internal open class EventEntity(@Index var eventId: String = "",
?.canBeProcessed = true ?.canBeProcessed = true
} }
/** fun isThread(): Boolean = rootThreadEventId != null
* Returns true if the current event is a thread root event
*/
fun isRootThread(): Boolean = isThread && rootThreadEventId == null
} }

View File

@ -33,11 +33,9 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken:
val query = where(realm, roomId) val query = where(realm, roomId)
if (prevToken != null) { if (prevToken != null) {
query.equalTo(ChunkEntityFields.PREV_TOKEN, prevToken) query.equalTo(ChunkEntityFields.PREV_TOKEN, prevToken)
query.isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
} }
if (nextToken != null) { if (nextToken != null) {
query.equalTo(ChunkEntityFields.NEXT_TOKEN, nextToken) query.equalTo(ChunkEntityFields.NEXT_TOKEN, nextToken)
query.isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
} }
return query.findFirst() return query.findFirst()
} }
@ -45,15 +43,15 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken:
internal fun ChunkEntity.Companion.findLastForwardChunkOfRoom(realm: Realm, roomId: String): ChunkEntity? { internal fun ChunkEntity.Companion.findLastForwardChunkOfRoom(realm: Realm, roomId: String): ChunkEntity? {
return where(realm, roomId) return where(realm, roomId)
.equalTo(ChunkEntityFields.IS_LAST_FORWARD, true) .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
.isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
.findFirst() .findFirst()
} }
internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds: List<String>): RealmResults<ChunkEntity> { internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds: List<String>): RealmResults<ChunkEntity> {
return realm.where<ChunkEntity>() return realm.where<ChunkEntity>()
.`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, eventIds.toTypedArray()) .`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, eventIds.toTypedArray())
.isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
.findAll() .findAll()
} }
@ -72,16 +70,3 @@ internal fun ChunkEntity.Companion.create(
this.nextToken = nextToken this.nextToken = nextToken
} }
} }
// Threads
internal fun ChunkEntity.Companion.findThreadChunkOfRoom(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity? {
return where(realm, roomId)
.equalTo(ChunkEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
.findFirst()
}
internal fun ChunkEntity.Companion.findAllThreadChunkOfRoom(realm: Realm, roomId: String): RealmResults<ChunkEntity> {
return where(realm, roomId)
.isNotNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
.findAll()
}

View File

@ -85,3 +85,8 @@ internal fun RealmList<EventEntity>.find(eventId: String): EventEntity? {
internal fun RealmList<EventEntity>.fastContains(eventId: String): Boolean { internal fun RealmList<EventEntity>.fastContains(eventId: String): Boolean {
return this.find(eventId) != null return this.find(eventId) != null
} }
internal fun EventEntity.Companion.whereRootThreadEventId(realm: Realm, rootThreadEventId: String): RealmQuery<EventEntity> {
return realm.where<EventEntity>()
.equalTo(EventEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
}

View File

@ -25,9 +25,11 @@ import io.realm.kotlin.where
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.timeline.TimelineEventFilters import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters
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.ChunkEntityFields
import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import timber.log.Timber
internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery<TimelineEventEntity> { internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery<TimelineEventEntity> {
return realm.where<TimelineEventEntity>() return realm.where<TimelineEventEntity>()
@ -59,6 +61,7 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
filters: TimelineEventFilters = TimelineEventFilters()): TimelineEventEntity? { filters: TimelineEventFilters = TimelineEventFilters()): TimelineEventEntity? {
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null
val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterEvents(filters) val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterEvents(filters)
val liveEvents = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.where()?.filterEvents(filters) val liveEvents = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.where()?.filterEvents(filters)
val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) { val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) {
sendingTimelineEvents sendingTimelineEvents
@ -100,6 +103,7 @@ internal fun RealmQuery<TimelineEventEntity>.filterEvents(filters: TimelineEvent
if (filters.filterRedacted) { if (filters.filterRedacted) {
not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED) not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
} }
return this return this
} }

View File

@ -101,7 +101,7 @@ internal class DefaultTimeline(
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>()) private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
private val backwardsState = AtomicReference(TimelineState()) private val backwardsState = AtomicReference(TimelineState())
private val forwardsState = AtomicReference(TimelineState()) private val forwardsState = AtomicReference(TimelineState())
private var isFromThreadTimeline = false
override val timelineID = UUID.randomUUID().toString() override val timelineID = UUID.randomUUID().toString()
override val isLive override val isLive
@ -143,8 +143,9 @@ internal class DefaultTimeline(
} }
} }
override fun start() { override fun start(rootThreadEventId: String?) {
if (isStarted.compareAndSet(false, true)) { if (isStarted.compareAndSet(false, true)) {
isFromThreadTimeline = rootThreadEventId != null
Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId") Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
timelineInput.listeners.add(this) timelineInput.listeners.add(this)
BACKGROUND_HANDLER.post { BACKGROUND_HANDLER.post {
@ -163,7 +164,13 @@ internal class DefaultTimeline(
postSnapshot() postSnapshot()
} }
timelineEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() timelineEvents = rootThreadEventId?.let {
TimelineEventEntity
.whereRoomId(realm, roomId = roomId)
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, it)
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
} ?: buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
timelineEvents.addChangeListener(eventsChangeListener) timelineEvents.addChangeListener(eventsChangeListener)
handleInitialLoad() handleInitialLoad()
loadRoomMembersTask loadRoomMembersTask
@ -313,16 +320,18 @@ internal class DefaultTimeline(
val firstCacheEvent = results.firstOrNull() val firstCacheEvent = results.firstOrNull()
val chunkEntity = getLiveChunk() val chunkEntity = getLiveChunk()
updateState(Timeline.Direction.FORWARDS) { updateState(Timeline.Direction.FORWARDS) {
it.copy( it.copy(
hasMoreInCache = !builtEventsIdMap.containsKey(firstCacheEvent?.eventId), hasMoreInCache = !builtEventsIdMap.containsKey(firstCacheEvent?.eventId), // what is in DB
hasReachedEnd = chunkEntity?.isLastForward ?: false hasReachedEnd = if (isFromThreadTimeline) true else chunkEntity?.isLastForward ?: false // if you neeed fetch more
) )
} }
updateState(Timeline.Direction.BACKWARDS) { updateState(Timeline.Direction.BACKWARDS) {
it.copy( it.copy(
hasMoreInCache = !builtEventsIdMap.containsKey(lastCacheEvent?.eventId), hasMoreInCache = !builtEventsIdMap.containsKey(lastCacheEvent?.eventId),
hasReachedEnd = chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE hasReachedEnd = if (isFromThreadTimeline) true else chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE
) )
} }
} }
@ -472,6 +481,7 @@ internal class DefaultTimeline(
* This has to be called on TimelineThread as it accesses realm live results * This has to be called on TimelineThread as it accesses realm live results
*/ */
private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
val currentChunk = getLiveChunk() val currentChunk = getLiveChunk()
val token = if (direction == Timeline.Direction.BACKWARDS) currentChunk?.prevToken else currentChunk?.nextToken val token = if (direction == Timeline.Direction.BACKWARDS) currentChunk?.prevToken else currentChunk?.nextToken
if (token == null) { if (token == null) {

View File

@ -21,6 +21,7 @@ import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.filter.FilterRepository import org.matrix.android.sdk.internal.session.filter.FilterRepository
import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal interface PaginationTask : Task<PaginationTask.Params, TokenChunkEventPersistor.Result> { internal interface PaginationTask : Task<PaginationTask.Params, TokenChunkEventPersistor.Result> {

View File

@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room.timeline
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
import org.matrix.android.sdk.BuildConfig
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.isThread import org.matrix.android.sdk.api.session.events.model.isThread
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
@ -28,10 +29,10 @@ import org.matrix.android.sdk.internal.database.helper.addIfNecessary
import org.matrix.android.sdk.internal.database.helper.addStateEvent import org.matrix.android.sdk.internal.database.helper.addStateEvent
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.merge import org.matrix.android.sdk.internal.database.helper.merge
import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.mapper.toEntity
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.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
@ -41,9 +42,9 @@ import org.matrix.android.sdk.internal.database.query.create
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.findAllIncludingEvents import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
import org.matrix.android.sdk.internal.database.query.findThreadChunkOfRoom
import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRootThreadEventId
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryEventsHelper import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryEventsHelper
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
@ -160,6 +161,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
handlePagination(realm, roomId, direction, receivedChunk, currentChunk) handlePagination(realm, roomId, direction, receivedChunk, currentChunk)
} }
} }
return if (receivedChunk.events.isEmpty()) { return if (receivedChunk.events.isEmpty()) {
if (receivedChunk.hasMore()) { if (receivedChunk.hasMore()) {
Result.SHOULD_FETCH_MORE Result.SHOULD_FETCH_MORE
@ -210,6 +212,8 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
} }
} }
val eventIds = ArrayList<String>(eventList.size) val eventIds = ArrayList<String>(eventList.size)
val optimizedThreadSummaryMap = hashMapOf<String,EventEntity>()
eventList.forEach { event -> eventList.forEach { event ->
if (event.eventId == null || event.senderId == null) { if (event.eventId == null || event.senderId == null) {
return@forEach return@forEach
@ -226,16 +230,18 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
roomMemberContentsByUser[event.stateKey] = contentToUse.toModel<RoomMemberContent>() roomMemberContentsByUser[event.stateKey] = contentToUse.toModel<RoomMemberContent>()
} }
Timber.i("------> [TokenChunkEventPersistor] Add TimelineEvent to chunkEntity event[${event.eventId}] ${if (event.isThread()) "is Thread" else ""}") currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
addTimelineEventToChunk( eventEntity.rootThreadEventId?.let {
realm = realm, // This is a thread event
roomId = roomId, optimizedThreadSummaryMap[it] = eventEntity
eventEntity = eventEntity, } ?: run {
currentChunk = currentChunk, // This is a normal event or a root thread one
direction = direction, optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
roomMemberContentsByUser = roomMemberContentsByUser)
} }
}
// Find all the chunks which contain at least one event from the list of eventIds // Find all the chunks which contain at least one event from the list of eventIds
val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds) val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds)
Timber.d("Found ${chunks.size} chunks containing at least one of the eventIds") Timber.d("Found ${chunks.size} chunks containing at least one of the eventIds")
@ -254,49 +260,63 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null || val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null ||
(chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS) (chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS)
if (shouldUpdateSummary) { if (shouldUpdateSummary) {
// TODO maybe add support to view latest thread message
roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
} }
if (currentChunk.isValid) { if (currentChunk.isValid) {
RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk) RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(currentChunk)
} }
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded()
} }
/** // /**
* Adds a timeline event to the correct chunk. If there is a thread detected will be added // * Mark or update the thread root event accordingly. If the Threading is disabled
* to a specific chunk // * no action is done
*/ // */
private fun addTimelineEventToChunk(realm: Realm, // private fun updateRootThreadEventIfNeeded(realm: Realm, eventEntity: EventEntity) {
roomId: String, //
eventEntity: EventEntity, // if (!BuildConfig.THREADING_ENABLED) return
currentChunk: ChunkEntity, //
direction: PaginationDirection, // val rootThreadEventId = eventEntity.rootThreadEventId
roomMemberContentsByUser: Map<String, RoomMemberContent?>) { //
val rootThreadEventId = eventEntity.rootThreadEventId // if (eventEntity.isThread && rootThreadEventId != null) {
if (eventEntity.isThread && rootThreadEventId != null) { // markEventAsRootEvent(realm, rootThreadEventId)
val threadChunk = getOrCreateThreadChunk(realm, roomId, rootThreadEventId) // } else {
threadChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser) // markAsRootEventIfNeeded(realm, eventEntity.eventId)
markEventAsRootEvent(realm, rootThreadEventId) // }
if (threadChunk.isValid) // }
RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(threadChunk)
} else {
currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
}
}
private fun markEventAsRootEvent(realm: Realm, rootThreadEventId: String) { // /**
val rootThreadEvent = EventEntity // * Finds the event with rootThreadEventId and marks it as a root thread
.where(realm, rootThreadEventId) // */
.equalTo(EventEntityFields.IS_THREAD, false).findFirst() ?: return // private fun markEventAsRootEvent(realm: Realm, rootThreadEventId: String) {
rootThreadEvent.isThread = true // val rootThreadEvent = EventEntity
} // .where(realm, rootThreadEventId)
// .equalTo(EventEntityFields.IS_THREAD, false).findFirst() ?: return
// rootThreadEvent.isThread = true
// }
//
// /**
// * Also check if there is at least one thread message for that rootThreadEventId,
// * that means it is a root thread so it should be updated accordingly
// */
// private fun markAsRootEventIfNeeded(realm: Realm, candidateIdRootThread: String) {
// EventEntity
// .whereRootThreadEventId(realm, candidateIdRootThread)
// .findFirst() ?: return
//
// markEventAsRootEvent(realm, candidateIdRootThread)
// }
/** // /**
* Returns the chunk for the current room if exists, otherwise it creates a new ChunkEntity // * Returns the chunk for the current room if exists, otherwise it creates a new ChunkEntity
*/ // */
private fun getOrCreateThreadChunk(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity { // private fun getOrCreateThreadChunk(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity {
return ChunkEntity.findThreadChunkOfRoom(realm, roomId, rootThreadEventId) // return ChunkEntity.findThreadChunkOfRoom(realm, roomId, rootThreadEventId)
?: realm.createObject<ChunkEntity>().apply { // ?: realm.createObject<ChunkEntity>().apply {
this.rootThreadEventId = rootThreadEventId // this.rootThreadEventId = rootThreadEventId
} // }
} // }
} }

View File

@ -21,41 +21,33 @@ import io.realm.kotlin.createObject
import org.matrix.android.sdk.api.session.crypto.MXCryptoError 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.isThread
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.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.model.message.MessageRelationContent
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.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
import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM 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.mapper.EventMapper import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
import org.matrix.android.sdk.internal.database.mapper.asDomain 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.mapper.toEntity
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.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.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.deleteOnCascade import org.matrix.android.sdk.internal.database.model.deleteOnCascade
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.findAllThreadChunkOfRoom
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
import org.matrix.android.sdk.internal.database.query.findThreadChunkOfRoom
import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.getOrNull
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
@ -68,11 +60,8 @@ 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
import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberEventHandler import org.matrix.android.sdk.internal.session.room.membership.RoomMemberEventHandler
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.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
import org.matrix.android.sdk.internal.session.room.timeline.TimelineInput import org.matrix.android.sdk.internal.session.room.timeline.TimelineInput
import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent
import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy
@ -357,11 +346,14 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
syncLocalTimestampMillis: Long, syncLocalTimestampMillis: Long,
aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity { aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId) val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
val chunkEntity = if (!isLimited && lastChunk != null) { val chunkEntity = if (!isLimited && lastChunk != null) {
// There are no more events to fetch
lastChunk lastChunk
} else { } else {
realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken } realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
} }
// Only one chunk has isLastForward set to true // Only one chunk has isLastForward set to true
lastChunk?.isLastForward = false lastChunk?.isLastForward = false
chunkEntity.isLastForward = true chunkEntity.isLastForward = true
@ -369,21 +361,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
val eventIds = ArrayList<String>(eventList.size) val eventIds = ArrayList<String>(eventList.size)
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>() val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
///////////////////// val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
// There is only one chunk per room
val threadChunks = ChunkEntity.findAllThreadChunkOfRoom(realm, roomId)
val tc = threadChunks.joinToString { chunk ->
var output = "\n----------------\n------> [${chunk.timelineEvents.size}] rootThreadEventId = ${chunk.rootThreadEventId}" + "\n"
output += chunk.timelineEvents
.joinToString("") {
"------> " + "eventId:[${it?.eventId}] payload:[${getValueFromPayload(it.root?.let { root -> EventMapper.map(root).mxDecryptionResult }?.payload, "body")}]\n"
}
output
}
Timber.i("------> Chunks (${threadChunks.size})$tc")
/////////////////////
for (event in eventList) { for (event in eventList) {
if (event.eventId == null || event.senderId == null || event.type == null) { if (event.eventId == null || event.senderId == null || event.type == null) {
continue continue
@ -413,15 +391,14 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
rootStateEvent?.asDomain()?.getFixedRoomMemberContent() rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
} }
Timber.i("------> [RoomSyncHandler] Add TimelineEvent to chunkEntity event[${event.eventId}] ${if (event.isThread()) "is Thread" else ""}") chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
eventEntity.rootThreadEventId?.let {
addTimelineEventToChunk( // This is a thread event
realm = realm, optimizedThreadSummaryMap[it] = eventEntity
roomId = roomId, } ?: run {
eventEntity = eventEntity, // This is a normal event or a root thread one
chunkEntity = chunkEntity, optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
roomEntity = roomEntity, }
roomMemberContentsByUser = roomMemberContentsByUser)
// Give info to crypto module // Give info to crypto module
cryptoService.onLiveEvent(roomEntity.roomId, event) cryptoService.onLiveEvent(roomEntity.roomId, event)
@ -447,56 +424,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
} }
} }
} }
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded()
// posting new events to timeline if any is registered // posting new events to timeline if any is registered
timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds) timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds)
return chunkEntity return chunkEntity
} }
/**
* Adds a timeline event to the correct chunk. If there is a thread detected will be added
* to a specific chunk
*/
private fun addTimelineEventToChunk(realm: Realm,
roomId: String,
eventEntity: EventEntity,
chunkEntity: ChunkEntity,
roomEntity: RoomEntity,
roomMemberContentsByUser: Map<String, RoomMemberContent?>) {
val rootThreadEventId = eventEntity.rootThreadEventId
if (eventEntity.isThread && rootThreadEventId != null) {
val threadChunk = getOrCreateThreadChunk(realm, roomId, rootThreadEventId)
threadChunk.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
markEventAsRootEvent(realm, rootThreadEventId)
roomEntity.addIfNecessary(threadChunk)
} else {
chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
}
}
@Suppress("UNCHECKED_CAST")
private fun getValueFromPayload(payload: JsonDict?, key: String): String? {
val content = payload?.get("content") as? JsonDict
return content?.get(key) as? String
}
/**
* Returns the chunk for the current room if exists, otherwise it creates a new ChunkEntity
*/
private fun getOrCreateThreadChunk(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity {
return ChunkEntity.findThreadChunkOfRoom(realm, roomId, rootThreadEventId)
?: realm.createObject<ChunkEntity>().apply {
this.rootThreadEventId = rootThreadEventId
}
}
private fun markEventAsRootEvent(realm: Realm, rootThreadEventId: String){
val rootThreadEvent = EventEntity
.where(realm, rootThreadEventId)
.equalTo(EventEntityFields.IS_THREAD, false).findFirst() ?: return
rootThreadEvent.isThread = true
}
private fun decryptIfNeeded(event: Event, roomId: String) { private fun decryptIfNeeded(event: Event, roomId: String) {
try { try {
// Event from sync does not have roomId, so add it to the event first // Event from sync does not have roomId, so add it to the event first

View File

@ -159,6 +159,9 @@ android {
// This *must* only be set in trusted environments. // This *must* only be set in trusted environments.
buildConfigField "Boolean", "handleCallAssertedIdentityEvents", "false" buildConfigField "Boolean", "handleCallAssertedIdentityEvents", "false"
// Indicates whether or not threading support is enabled
buildConfigField "Boolean", "THREADING_ENABLED", "${isThreadingEnabled}"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// Keep abiFilter for the universalApk // Keep abiFilter for the universalApk

View File

@ -359,7 +359,6 @@ class RoomDetailFragment @Inject constructor(
} else { } else {
setupToolbar(views.roomToolbar) setupToolbar(views.roomToolbar)
} }
setupThreadIfNeeded()
setupRecyclerView() setupRecyclerView()
setupComposer() setupComposer()
setupNotificationView() setupNotificationView()
@ -1194,12 +1193,6 @@ class RoomDetailFragment @Inject constructor(
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************
private fun setupThreadIfNeeded(){
getRootThreadEventId()?.let{
textComposerViewModel.handle(TextComposerAction.EnterReplyInThreadTimeline(it))
}
}
private fun setupRecyclerView() { private fun setupRecyclerView() {
timelineEventController.callback = this timelineEventController.callback = this
timelineEventController.timeline = roomDetailViewModel.timeline timelineEventController.timeline = roomDetailViewModel.timeline
@ -1762,7 +1755,7 @@ class RoomDetailFragment @Inject constructor(
this.view?.hideKeyboard() this.view?.hideKeyboard()
MessageActionsBottomSheet MessageActionsBottomSheet
.newInstance(roomId, informationData) .newInstance(roomId, informationData, isThreadTimeLine())
.show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS") .show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS")
return true return true

View File

@ -160,7 +160,7 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
init { init {
timeline.start() timeline.start(initialState.rootThreadEventId)
timeline.addListener(this) timeline.addListener(this)
observeRoomSummary() observeRoomSummary()
observeMembershipChanges() observeMembershipChanges()
@ -1094,6 +1094,7 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
timelineEvents.tryEmit(snapshot) timelineEvents.tryEmit(snapshot)
// PreviewUrl // PreviewUrl

View File

@ -73,7 +73,8 @@ data class RoomDetailViewState(
roomId = args.roomId, roomId = args.roomId,
eventId = args.eventId, eventId = args.eventId,
// Also highlight the target event, if any // Also highlight the target event, if any
highlightedEventId = args.eventId highlightedEventId = args.eventId,
rootThreadEventId = args.roomThreadDetailArgs?.eventId
) )
fun isWebRTCCallOptionAvailable() = (asyncRoomSummary.invoke()?.joinedMembersCount ?: 0) <= 2 fun isWebRTCCallOptionAvailable() = (asyncRoomSummary.invoke()?.joinedMembersCount ?: 0) <= 2

View File

@ -89,8 +89,6 @@ class TextComposerViewModel @AssistedInject constructor(
is TextComposerAction.UserIsTyping -> handleUserIsTyping(action) is TextComposerAction.UserIsTyping -> handleUserIsTyping(action)
is TextComposerAction.OnTextChanged -> handleOnTextChanged(action) is TextComposerAction.OnTextChanged -> handleOnTextChanged(action)
is TextComposerAction.OnVoiceRecordingStateChanged -> handleOnVoiceRecordingStateChanged(action) is TextComposerAction.OnVoiceRecordingStateChanged -> handleOnVoiceRecordingStateChanged(action)
is TextComposerAction.EnterReplyInThreadTimeline -> handleEnterReplyInThreadTimeline(action)
} }
} }
@ -98,10 +96,6 @@ class TextComposerViewModel @AssistedInject constructor(
copy(isVoiceRecording = action.isRecording) copy(isVoiceRecording = action.isRecording)
} }
private fun handleEnterReplyInThreadTimeline(action: TextComposerAction.EnterReplyInThreadTimeline) = setState {
copy(rootThreadEventId = action.rootThreadEventId)
}
private fun handleOnTextChanged(action: TextComposerAction.OnTextChanged) { private fun handleOnTextChanged(action: TextComposerAction.OnTextChanged) {
setState { setState {
// Makes sure currentComposerText is upToDate when accessing further setState // Makes sure currentComposerText is upToDate when accessing further setState

View File

@ -53,7 +53,9 @@ data class TextComposerViewState(
val isComposerVisible: Boolean val isComposerVisible: Boolean
get() = canSendMessage && !isVoiceRecording get() = canSendMessage && !isVoiceRecording
constructor(args: RoomDetailArgs) : this(roomId = args.roomId) constructor(args: RoomDetailArgs) : this(
roomId = args.roomId,
rootThreadEventId = args.roomThreadDetailArgs?.eventId)
fun isInThreadTimeline(): Boolean = rootThreadEventId != null fun isInThreadTimeline(): Boolean = rootThreadEventId != null
} }

View File

@ -93,14 +93,16 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val unreadState: UnreadState = UnreadState.Unknown, val unreadState: UnreadState = UnreadState.Unknown,
val highlightedEventId: String? = null, val highlightedEventId: String? = null,
val jitsiState: JitsiState = JitsiState(), val jitsiState: JitsiState = JitsiState(),
val roomSummary: RoomSummary? = null val roomSummary: RoomSummary? = null,
val rootThreadEventId: String? = null
) { ) {
constructor(state: RoomDetailViewState) : this( constructor(state: RoomDetailViewState) : this(
unreadState = state.unreadState, unreadState = state.unreadState,
highlightedEventId = state.highlightedEventId, highlightedEventId = state.highlightedEventId,
jitsiState = state.jitsiState, jitsiState = state.jitsiState,
roomSummary = state.asyncRoomSummary() roomSummary = state.asyncRoomSummary(),
rootThreadEventId = state.rootThreadEventId
) )
} }
@ -191,7 +193,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
// it's sent by the same user so we are sure we have up to date information. // it's sent by the same user so we are sure we have up to date information.
val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId
val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast { val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast {
timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId) timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId, partialState.rootThreadEventId )
} }
if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) { if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) {
modelCache[prevDisplayableEventIndex] = null modelCache[prevDisplayableEventIndex] = null
@ -319,6 +321,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
} }
private fun submitSnapshot(newSnapshot: List<TimelineEvent>) { private fun submitSnapshot(newSnapshot: List<TimelineEvent>) {
// Update is triggered on any DB change
backgroundHandler.post { backgroundHandler.post {
inSubmitList = true inSubmitList = true
val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot) val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot)
@ -367,7 +370,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val nextEvent = currentSnapshot.nextOrNull(position) val nextEvent = currentSnapshot.nextOrNull(position)
val prevEvent = currentSnapshot.prevOrNull(position) val prevEvent = currentSnapshot.prevOrNull(position)
val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull { val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId) timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId, partialState.rootThreadEventId)
} }
// Should be build if not cached or if model should be refreshed // Should be build if not cached or if model should be refreshed
if (modelCache[position] == null || modelCache[position]?.isCacheable == false) { if (modelCache[position] == null || modelCache[position]?.isCacheable == false) {
@ -449,7 +452,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
return null return null
} }
// If the event is not shown, we go to the next one // If the event is not shown, we go to the next one
if (!timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId)) { if (!timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId, partialState.rootThreadEventId)) {
continue continue
} }
// If the event is sent by us, we update the holder with the eventId and stop the search // If the event is sent by us, we update the holder with the eventId and stop the search
@ -471,7 +474,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val currentReadReceipts = ArrayList(event.readReceipts).filter { val currentReadReceipts = ArrayList(event.readReceipts).filter {
it.user.userId != session.myUserId it.user.userId != session.myUserId
} }
if (timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId)) { if (timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId, partialState.rootThreadEventId)) {
lastShownEventId = event.eventId lastShownEventId = event.eventId
} }
if (lastShownEventId == null) { if (lastShownEventId == null) {

View File

@ -49,10 +49,15 @@ data class MessageActionState(
// For actions // For actions
val actions: List<EventSharedAction> = emptyList(), val actions: List<EventSharedAction> = emptyList(),
val expendedReportContentMenu: Boolean = false, val expendedReportContentMenu: Boolean = false,
val actionPermissions: ActionPermissions = ActionPermissions() val actionPermissions: ActionPermissions = ActionPermissions(),
val isFromThreadTimeline: Boolean = false
) : MavericksState { ) : MavericksState {
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData) constructor(args: TimelineEventFragmentArgs) : this(
roomId = args.roomId,
eventId = args.eventId,
informationData = args.informationData,
isFromThreadTimeline = args.isFromThreadTimeline)
fun senderName(): String = informationData.memberName?.toString() ?: "" fun senderName(): String = informationData.memberName?.toString() ?: ""

View File

@ -97,13 +97,14 @@ class MessageActionsBottomSheet :
} }
companion object { companion object {
fun newInstance(roomId: String, informationData: MessageInformationData): MessageActionsBottomSheet { fun newInstance(roomId: String, informationData: MessageInformationData, isFromThreadTimeline: Boolean): MessageActionsBottomSheet {
return MessageActionsBottomSheet().apply { return MessageActionsBottomSheet().apply {
setArguments( setArguments(
TimelineEventFragmentArgs( TimelineEventFragmentArgs(
informationData.eventId, informationData.eventId,
roomId, roomId,
informationData informationData,
isFromThreadTimeline
) )
) )
} }

View File

@ -22,6 +22,7 @@ import dagger.Lazy
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import im.vector.app.BuildConfig
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.canReact import im.vector.app.core.extensions.canReact
@ -326,7 +327,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
add(EventSharedAction.Reply(eventId)) add(EventSharedAction.Reply(eventId))
} }
// *** Testing Threads ****
if (canReplyInThread(timelineEvent, messageContent, actionPermissions)) { if (canReplyInThread(timelineEvent, messageContent, actionPermissions)) {
add(EventSharedAction.ReplyInThread(eventId)) add(EventSharedAction.ReplyInThread(eventId))
} }
@ -417,18 +417,22 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
} }
} }
private fun canReplyInThread(event: TimelineEvent, messageContent: MessageContent?, actionPermissions: ActionPermissions): Boolean { private fun canReplyInThread(event: TimelineEvent,
messageContent: MessageContent?,
actionPermissions: ActionPermissions): Boolean {
// Only event of type EventType.MESSAGE are supported for the moment // Only event of type EventType.MESSAGE are supported for the moment
if (!BuildConfig.THREADING_ENABLED) return false
if (initialState.isFromThreadTimeline) return false
if (event.root.getClearType() != EventType.MESSAGE) return false if (event.root.getClearType() != EventType.MESSAGE) return false
if (!actionPermissions.canSendMessage) return false if (!actionPermissions.canSendMessage) return false
return when (messageContent?.msgType) { return when (messageContent?.msgType) {
MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_TEXT -> true
MessageType.MSGTYPE_NOTICE, // MessageType.MSGTYPE_NOTICE,
MessageType.MSGTYPE_EMOTE, // MessageType.MSGTYPE_EMOTE,
MessageType.MSGTYPE_IMAGE, // MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_VIDEO, // MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_AUDIO, // MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_FILE -> true // MessageType.MSGTYPE_FILE -> true
else -> false else -> false
} }
} }

View File

@ -24,5 +24,6 @@ import kotlinx.parcelize.Parcelize
data class TimelineEventFragmentArgs( data class TimelineEventFragmentArgs(
val eventId: String, val eventId: String,
val roomId: String, val roomId: String,
val informationData: MessageInformationData val informationData: MessageInformationData,
val isFromThreadTimeline: Boolean = false
) : Parcelable ) : Parcelable

View File

@ -83,7 +83,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
eventIdToHighlight: String?, eventIdToHighlight: String?,
requestModelBuild: () -> Unit, requestModelBuild: () -> Unit,
callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? { callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? {
val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2, eventIdToHighlight) val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2, eventIdToHighlight, partialState.rootThreadEventId)
return if (mergedEvents.isEmpty()) { return if (mergedEvents.isEmpty()) {
null null
} else { } else {

View File

@ -149,7 +149,7 @@ class MessageItemFactory @Inject constructor(
// This is an edit event, we should display it when debugging as a notice event // This is an edit event, we should display it when debugging as a notice event
return noticeItemFactory.create(params) return noticeItemFactory.create(params)
} }
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, event.root.isRootThread) val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, event.root.threadDetails)
// val all = event.root.toContent() // val all = event.root.toContent()
// val ev = all.toModel<Event>() // val ev = all.toModel<Event>()

View File

@ -42,8 +42,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*> { fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*> {
val event = params.event val event = params.event
val computedModel = try { val computedModel = try {
if (!timelineEventVisibilityHelper.shouldShowEvent(event, params.highlightedEventId)) { if (!timelineEventVisibilityHelper.shouldShowEvent(event, params.highlightedEventId, params.rootThreadEventId)) {
return buildEmptyItem(event, params.prevEvent, params.highlightedEventId) return buildEmptyItem(event, params.prevEvent, params.highlightedEventId, params.rootThreadEventId)
} }
when (event.root.getClearType()) { when (event.root.getClearType()) {
// Message itemsX // Message itemsX
@ -109,11 +109,11 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
Timber.e(throwable, "failed to create message item") Timber.e(throwable, "failed to create message item")
defaultItemFactory.create(params, throwable) defaultItemFactory.create(params, throwable)
} }
return computedModel ?: buildEmptyItem(event, params.prevEvent, params.highlightedEventId) return computedModel ?: buildEmptyItem(event, params.prevEvent, params.highlightedEventId, params.rootThreadEventId)
} }
private fun buildEmptyItem(timelineEvent: TimelineEvent, prevEvent: TimelineEvent?, highlightedEventId: String?): TimelineEmptyItem { private fun buildEmptyItem(timelineEvent: TimelineEvent, prevEvent: TimelineEvent?, highlightedEventId: String?, rootThreadEventId: String?): TimelineEmptyItem {
val isNotBlank = prevEvent == null || timelineEventVisibilityHelper.shouldShowEvent(prevEvent, highlightedEventId) val isNotBlank = prevEvent == null || timelineEventVisibilityHelper.shouldShowEvent(prevEvent, highlightedEventId, rootThreadEventId)
return TimelineEmptyItem_() return TimelineEmptyItem_()
.id(timelineEvent.localId) .id(timelineEvent.localId)
.eventId(timelineEvent.eventId) .eventId(timelineEvent.eventId)

View File

@ -34,5 +34,8 @@ data class TimelineItemFactoryParams(
val highlightedEventId: String? val highlightedEventId: String?
get() = partialState.highlightedEventId get() = partialState.highlightedEventId
val rootThreadEventId: String?
get() = partialState.rootThreadEventId
val isHighlighted = highlightedEventId == event.eventId val isHighlighted = highlightedEventId == event.eventId
} }

View File

@ -21,6 +21,7 @@ import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import org.matrix.android.sdk.api.session.threads.ThreadDetails
import javax.inject.Inject import javax.inject.Inject
class MessageItemAttributesFactory @Inject constructor( class MessageItemAttributesFactory @Inject constructor(
@ -32,7 +33,7 @@ class MessageItemAttributesFactory @Inject constructor(
fun create(messageContent: Any?, fun create(messageContent: Any?,
informationData: MessageInformationData, informationData: MessageInformationData,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
isRootThread: Boolean = false): AbsMessageItem.Attributes { threadDetails: ThreadDetails? = null): AbsMessageItem.Attributes {
return AbsMessageItem.Attributes( return AbsMessageItem.Attributes(
avatarSize = avatarSizeProvider.avatarSize, avatarSize = avatarSizeProvider.avatarSize,
informationData = informationData, informationData = informationData,
@ -51,7 +52,7 @@ class MessageItemAttributesFactory @Inject constructor(
avatarCallback = callback, avatarCallback = callback,
readReceiptsCallback = callback, readReceiptsCallback = callback,
emojiTypeFace = emojiCompatFontProvider.typeface, emojiTypeFace = emojiCompatFontProvider.typeface,
isRootThread = isRootThread threadDetails = threadDetails
) )
} }
} }

View File

@ -18,9 +18,12 @@ package im.vector.app.features.home.room.detail.timeline.helper
import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.core.resources.UserPreferencesProvider
import org.matrix.android.sdk.BuildConfig
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 org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.getRelationContent
import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
import org.matrix.android.sdk.api.session.events.model.isThread
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.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
@ -37,7 +40,7 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
* *
* @return a list of timeline events which have sequentially the same type following the next direction. * @return a list of timeline events which have sequentially the same type following the next direction.
*/ */
fun nextSameTypeEvents(timelineEvents: List<TimelineEvent>, index: Int, minSize: Int, eventIdToHighlight: String?): List<TimelineEvent> { private fun nextSameTypeEvents(timelineEvents: List<TimelineEvent>, index: Int, minSize: Int, eventIdToHighlight: String?, rootThreadEventId: String?): List<TimelineEvent> {
if (index >= timelineEvents.size - 1) { if (index >= timelineEvents.size - 1) {
return emptyList() return emptyList()
} }
@ -59,7 +62,7 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
} else { } else {
nextSameDayEvents.subList(0, indexOfFirstDifferentEventType) nextSameDayEvents.subList(0, indexOfFirstDifferentEventType)
} }
val filteredSameTypeEvents = sameTypeEvents.filter { shouldShowEvent(it, eventIdToHighlight) } val filteredSameTypeEvents = sameTypeEvents.filter { shouldShowEvent(it, eventIdToHighlight, rootThreadEventId) }
if (filteredSameTypeEvents.size < minSize) { if (filteredSameTypeEvents.size < minSize) {
return emptyList() return emptyList()
} }
@ -74,12 +77,12 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
* *
* @return a list of timeline events which have sequentially the same type following the prev direction. * @return a list of timeline events which have sequentially the same type following the prev direction.
*/ */
fun prevSameTypeEvents(timelineEvents: List<TimelineEvent>, index: Int, minSize: Int, eventIdToHighlight: String?): List<TimelineEvent> { fun prevSameTypeEvents(timelineEvents: List<TimelineEvent>, index: Int, minSize: Int, eventIdToHighlight: String?, rootThreadEventId: String?): List<TimelineEvent> {
val prevSub = timelineEvents.subList(0, index + 1) val prevSub = timelineEvents.subList(0, index + 1)
return prevSub return prevSub
.reversed() .reversed()
.let { .let {
nextSameTypeEvents(it, 0, minSize, eventIdToHighlight) nextSameTypeEvents(it, 0, minSize, eventIdToHighlight, rootThreadEventId)
} }
} }
@ -88,7 +91,7 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
* @param highlightedEventId can be checked to force visibility to true * @param highlightedEventId can be checked to force visibility to true
* @return true if the event should be shown in the timeline. * @return true if the event should be shown in the timeline.
*/ */
fun shouldShowEvent(timelineEvent: TimelineEvent, highlightedEventId: String?): Boolean { fun shouldShowEvent(timelineEvent: TimelineEvent, highlightedEventId: String?, rootThreadEventId: String?): Boolean {
// If show hidden events is true we should always display something // If show hidden events is true we should always display something
if (userPreferencesProvider.shouldShowHiddenEvents()) { if (userPreferencesProvider.shouldShowHiddenEvents()) {
return true return true
@ -100,15 +103,16 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
if (!timelineEvent.isDisplayable()) { if (!timelineEvent.isDisplayable()) {
return false return false
} }
// Check for special case where we should hide the event, like redacted, relation, memberships... according to user preferences. // Check for special case where we should hide the event, like redacted, relation, memberships... according to user preferences.
return !timelineEvent.shouldBeHidden() return !timelineEvent.shouldBeHidden(rootThreadEventId)
} }
private fun TimelineEvent.isDisplayable(): Boolean { private fun TimelineEvent.isDisplayable(): Boolean {
return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.getClearType()) return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.getClearType())
} }
private fun TimelineEvent.shouldBeHidden(): Boolean { private fun TimelineEvent.shouldBeHidden(rootThreadEventId: String?): Boolean {
if (root.isRedacted() && !userPreferencesProvider.shouldShowRedactedMessages()) { if (root.isRedacted() && !userPreferencesProvider.shouldShowRedactedMessages()) {
return true return true
} }
@ -120,6 +124,11 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
if ((diff.isJoin || diff.isPart) && !userPreferencesProvider.shouldShowJoinLeaves()) return true if ((diff.isJoin || diff.isPart) && !userPreferencesProvider.shouldShowJoinLeaves()) return true
if ((diff.isAvatarChange || diff.isDisplaynameChange) && !userPreferencesProvider.shouldShowAvatarDisplayNameChanges()) return true if ((diff.isAvatarChange || diff.isDisplaynameChange) && !userPreferencesProvider.shouldShowAvatarDisplayNameChanges()) return true
} }
if(BuildConfig.THREADING_ENABLED && rootThreadEventId == null && root.isThread() && root.getRootThreadEventId() != null){
return true
}
return false return false
} }

View File

@ -18,10 +18,12 @@ package im.vector.app.features.home.room.detail.timeline.item
import android.graphics.Typeface import android.graphics.Typeface
import android.view.View import android.view.View
import android.view.ViewStub
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
@ -32,6 +34,9 @@ import im.vector.app.core.ui.views.SendStateImageView
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import org.matrix.android.sdk.api.session.threads.ThreadDetails
import org.matrix.android.sdk.api.util.MatrixItem
import timber.log.Timber
/** /**
* Base timeline item that adds an optional information bar with the sender avatar, name, time, send state * Base timeline item that adds an optional information bar with the sender avatar, name, time, send state
@ -98,8 +103,19 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
// Render send state indicator // Render send state indicator
holder.sendStateImageView.render(attributes.informationData.sendStateDecoration) holder.sendStateImageView.render(attributes.informationData.sendStateDecoration)
holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
holder.isThread.isVisible = attributes.isRootThread
// Threads
attributes.threadDetails?.let { threadDetails ->
threadDetails.isRootThread
holder.threadSummaryConstraintLayout.isVisible = threadDetails.isRootThread
holder.threadSummaryCounterTextView.text = threadDetails.numberOfThreads.toString()
holder.threadSummaryInfoTextView.text = threadDetails.threadSummaryLatestTextMessage
threadDetails.threadSummarySenderInfo?.let { senderInfo ->
attributes.avatarRenderer.render(MatrixItem.UserItem(senderInfo.userId, senderInfo.displayName, senderInfo.avatarUrl), holder.threadSummaryAvatarImageView)
} }
}
}
override fun unbind(holder: H) { override fun unbind(holder: H) {
attributes.avatarRenderer.clear(holder.avatarImageView) attributes.avatarRenderer.clear(holder.avatarImageView)
@ -118,7 +134,11 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
val timeView by bind<TextView>(R.id.messageTimeView) val timeView by bind<TextView>(R.id.messageTimeView)
val sendStateImageView by bind<SendStateImageView>(R.id.messageSendStateImageView) val sendStateImageView by bind<SendStateImageView>(R.id.messageSendStateImageView)
val eventSendingIndicator by bind<ProgressBar>(R.id.eventSendingIndicator) val eventSendingIndicator by bind<ProgressBar>(R.id.eventSendingIndicator)
val isThread by bind<View>(R.id.messageIsThread) val threadSummaryConstraintLayout by bind<ConstraintLayout>(R.id.messageThreadSummaryConstraintLayout)
val threadSummaryCounterTextView by bind<TextView>(R.id.messageThreadSummaryCounterTextView)
val threadSummaryImageView by bind<ImageView>(R.id.messageThreadSummaryImageView)
val threadSummaryAvatarImageView by bind<ImageView>(R.id.messageThreadSummaryAvatarImageView)
val threadSummaryInfoTextView by bind<TextView>(R.id.messageThreadSummaryInfoTextView)
} }
/** /**
@ -136,7 +156,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
val avatarCallback: TimelineEventController.AvatarCallback? = null, val avatarCallback: TimelineEventController.AvatarCallback? = null,
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
val emojiTypeFace: Typeface? = null, val emojiTypeFace: Typeface? = null,
val isRootThread: Boolean = false val threadDetails: ThreadDetails? = null
) : AbsBaseMessageItem.Attributes { ) : AbsBaseMessageItem.Attributes {
// Have to override as it's used to diff epoxy items // Have to override as it's used to diff epoxy items
@ -148,6 +168,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
if (avatarSize != other.avatarSize) return false if (avatarSize != other.avatarSize) return false
if (informationData != other.informationData) return false if (informationData != other.informationData) return false
if (threadDetails != other.threadDetails) return false
return true return true
} }
@ -155,6 +176,8 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
override fun hashCode(): Int { override fun hashCode(): Int {
var result = avatarSize var result = avatarSize
result = 31 * result + informationData.hashCode() result = 31 * result + informationData.hashCode()
result = 31 * result + threadDetails.hashCode()
return result return result
} }
} }

View File

@ -110,7 +110,7 @@ class MergedTimelines(
secondaryTimeline.removeAllListeners() secondaryTimeline.removeAllListeners()
} }
override fun start() { override fun start(rootThreadEventId: String?) {
mainTimeline.start() mainTimeline.start()
secondaryTimeline.start() secondaryTimeline.start()
} }

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,11 @@
<vector android:height="16dp" android:viewportHeight="18"
android:viewportWidth="18" android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#737D8C" android:pathData="M14.9995,1H2.9995C1.8949,1 0.9995,1.8954 0.9995,3V14V17L4.4662,14.4C4.8124,14.1404 5.2334,14 5.6662,14H6.9995H14.9995C16.1041,14 16.9995,13.1046 16.9995,12V8V2.9994C16.9995,1.8948 16.1041,1 14.9995,1Z"/>
<path android:fillColor="#737D8C" android:pathData="M4.4662,14.4L4.0162,13.8H4.0162L4.4662,14.4ZM0.9995,17H0.2495C0.2495,17.2841 0.41,17.5438 0.6641,17.6708C0.9182,17.7979 1.2222,17.7704 1.4495,17.6L0.9995,17ZM2.9995,1.75H14.9995V0.25H2.9995V1.75ZM1.7495,14V3H0.2495V14H1.7495ZM16.2495,2.9994V8H17.7495V2.9994H16.2495ZM4.0162,13.8L0.5495,16.4L1.4495,17.6L4.9162,15L4.0162,13.8ZM1.7495,17V14H0.2495V17H1.7495ZM5.6662,14.75H6.9995V13.25H5.6662V14.75ZM6.9995,14.75H14.9995V13.25H6.9995V14.75ZM17.7495,12V8H16.2495V12H17.7495ZM14.9995,14.75C16.5183,14.75 17.7495,13.5188 17.7495,12H16.2495C16.2495,12.6904 15.6899,13.25 14.9995,13.25V14.75ZM4.9162,15C5.1325,14.8377 5.3957,14.75 5.6662,14.75V13.25C5.0712,13.25 4.4922,13.443 4.0162,13.8L4.9162,15ZM14.9995,1.75C15.6902,1.75 16.2495,2.3093 16.2495,2.9994H17.7495C17.7495,1.4803 16.518,0.25 14.9995,0.25V1.75ZM2.9995,0.25C1.4807,0.25 0.2495,1.4812 0.2495,3H1.7495C1.7495,2.3096 2.3092,1.75 2.9995,1.75V0.25Z"/>
<path android:fillColor="#00000000"
android:pathData="M4.9995,6C4.9995,6 9.0943,6 12.9995,6"
android:strokeColor="#F4F6FA" android:strokeLineCap="round" android:strokeWidth="1.5"/>
<path android:fillColor="#00000000"
android:pathData="M4.9995,9H8.9995"
android:strokeColor="#F4F6FA" android:strokeLineCap="round" android:strokeWidth="1.5"/>
</vector>

View File

@ -33,8 +33,8 @@
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:layout_toStartOf="@+id/messageTimeView" android:layout_toStartOf="@id/messageTimeView"
android:layout_toEndOf="@+id/messageStartGuideline" android:layout_toEndOf="@id/messageStartGuideline"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textColor="?vctr_content_primary" android:textColor="?vctr_content_primary"
@ -200,17 +200,7 @@
</com.google.android.flexbox.FlexboxLayout> </com.google.android.flexbox.FlexboxLayout>
</LinearLayout> </LinearLayout>
<View <include
android:id="@+id/messageIsThread" layout="@layout/view_thread_room_summary" />
android:layout_width="wrap_content"
android:background="#2653AE"
android:layout_height="2dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="4dp"
android:layout_below="@id/informationBottom"
android:layout_toStartOf="@id/messageSendStateImageView"
android:layout_toEndOf="@id/messageStartGuideline"
android:contentDescription="@string/room_threads_filter" />
</RelativeLayout> </RelativeLayout>

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/messageThreadSummaryConstraintLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/informationBottom"
android:layout_marginEnd="32dp"
android:layout_marginBottom="4dp"
android:layout_toEndOf="@id/messageStartGuideline"
android:background="@drawable/rounded_rect_shape_8"
android:contentDescription="@string/room_threads_filter"
android:maxWidth="496dp"
android:minWidth="144dp"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:visibility="gone"
tools:visibility="visible">
<ImageView
android:id="@+id/messageThreadSummaryImageView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="13dp"
android:layout_marginTop="2dp"
android:contentDescription="@string/room_threads_filter"
android:src="@drawable/ic_thread_summary" />
<TextView
android:id="@+id/messageThreadSummaryCounterTextView"
style="@style/Widget.Vector.TextView.Caption"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/messageThreadSummaryImageView"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:textColor="?vctr_content_secondary"
tools:text="187" />
<ImageView
android:id="@+id/messageThreadSummaryAvatarImageView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/messageThreadSummaryCounterTextView"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="13dp"
android:contentDescription="@string/avatar"
tools:src="@sample/user_round_avatars" />
<TextView
android:id="@+id/messageThreadSummaryInfoTextView"
style="@style/Widget.Vector.TextView.Caption"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/messageThreadSummaryAvatarImageView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_default="wrap"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="13dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?vctr_content_secondary"
tools:text="Hello There, whats up! Its a large centence" />
</androidx.constraintlayout.widget.ConstraintLayout>