- Thread Summary along with optimization
- Create new thread & reply to thread
This commit is contained in:
parent
ecc9b59ad1
commit
8c539426e6
@ -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 {
|
||||||
|
@ -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'
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
)
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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> {
|
||||||
|
@ -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
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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() ?: ""
|
||||||
|
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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>()
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
11
vector/src/main/res/drawable/ic_thread_summary.xml
Normal file
11
vector/src/main/res/drawable/ic_thread_summary.xml
Normal 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>
|
@ -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>
|
74
vector/src/main/res/layout/view_thread_room_summary.xml
Normal file
74
vector/src/main/res/layout/view_thread_room_summary.xml
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user