- 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") {
|
||||
// sonarqube {
|
||||
// properties {
|
||||
|
@ -38,6 +38,8 @@ android {
|
||||
resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
|
||||
resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\""
|
||||
|
||||
// Indicates whether or not threading support is enabled
|
||||
buildConfigField "Boolean", "THREADING_ENABLED", "${isThreadingEnabled}"
|
||||
defaultConfig {
|
||||
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.relation.RelationDefaultContent
|
||||
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.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||
@ -97,7 +98,7 @@ data class Event(
|
||||
var sendStateDetails: String? = null
|
||||
|
||||
@Transient
|
||||
var isRootThread: Boolean = false
|
||||
var threadDetails: ThreadDetails? = null
|
||||
|
||||
fun sendStateError(): MatrixError? {
|
||||
return sendStateDetails?.let {
|
||||
@ -124,6 +125,7 @@ data class Event(
|
||||
it.mCryptoErrorReason = mCryptoErrorReason
|
||||
it.sendState = sendState
|
||||
it.ageLocalTs = ageLocalTs
|
||||
it.threadDetails = threadDetails
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,6 +188,16 @@ data class Event(
|
||||
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
|
||||
*/
|
||||
@ -218,7 +230,7 @@ data class Event(
|
||||
if (mCryptoError != other.mCryptoError) return false
|
||||
if (mCryptoErrorReason != other.mCryptoErrorReason) return false
|
||||
if (sendState != other.sendState) return false
|
||||
|
||||
if (threadDetails != other.threadDetails) return false
|
||||
return true
|
||||
}
|
||||
|
||||
@ -237,6 +249,8 @@ data class Event(
|
||||
result = 31 * result + (mCryptoError?.hashCode() ?: 0)
|
||||
result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0)
|
||||
result = 31 * result + sendState.hashCode()
|
||||
result = 31 * result + threadDetails.hashCode()
|
||||
|
||||
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
|
||||
*/
|
||||
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.
|
||||
|
@ -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) {
|
||||
Timber.d("Step 18 -> 19")
|
||||
val eventEntity = realm.schema.get("TimelineEventEntity") ?: return
|
||||
|
||||
realm.schema.get("EventEntity")
|
||||
?.addField(EventEntityFields.IS_THREAD, Boolean::class.java, FieldAttribute.INDEXED)
|
||||
?.addField(EventEntityFields.ROOT_THREAD_EVENT_ID, String::class.java)
|
||||
realm.schema.get("ChunkEntity")
|
||||
?.addField(ChunkEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
|
||||
?.addField(EventEntityFields.IS_ROOT_THREAD, Boolean::class.java, FieldAttribute.INDEXED)
|
||||
?.addField(EventEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
|
||||
?.addField(EventEntityFields.NUMBER_OF_THREADS, Int::class.java)
|
||||
?.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.isThread
|
||||
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.database.model.EventEntity
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
@ -41,8 +44,6 @@ internal object EventMapper {
|
||||
eventEntity.isUseless = IsUselessResolver.isUseless(event)
|
||||
eventEntity.stateKey = event.stateKey
|
||||
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.originServerTs = event.originServerTs
|
||||
eventEntity.redacts = event.redacts
|
||||
@ -55,6 +56,9 @@ internal object EventMapper {
|
||||
}
|
||||
eventEntity.decryptionErrorReason = event.mCryptoErrorReason
|
||||
eventEntity.decryptionErrorCode = event.mCryptoError?.name
|
||||
eventEntity.isRootThread = event.threadDetails?.isRootThread ?: false
|
||||
eventEntity.rootThreadEventId = event.getRootThreadEventId()
|
||||
eventEntity.numberOfThreads = event.threadDetails?.numberOfThreads ?: 0
|
||||
return eventEntity
|
||||
}
|
||||
|
||||
@ -97,7 +101,20 @@ internal object EventMapper {
|
||||
MXCryptoError.ErrorType.valueOf(errorCode)
|
||||
}
|
||||
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,
|
||||
// Because of gaps we can have several chunks with nextToken == null
|
||||
@Index var nextToken: String? = null,
|
||||
@Index var rootThreadEventId: String? = null,
|
||||
var stateEvents: RealmList<EventEntity> = RealmList(),
|
||||
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
||||
var numberOfTimelineEvents: Long = 0,
|
||||
@ -46,7 +45,6 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
|
||||
|
||||
companion object
|
||||
|
||||
fun isThreadChunk() = rootThreadEventId != null
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRoot: Boolean) {
|
||||
|
@ -27,15 +27,13 @@ import org.matrix.android.sdk.internal.extensions.assertIsManaged
|
||||
internal open class EventEntity(@Index var eventId: String = "",
|
||||
@Index var roomId: String = "",
|
||||
@Index var type: String = "",
|
||||
@Index var isThread: Boolean = false,
|
||||
var rootThreadEventId: String? = null,
|
||||
var content: String? = null,
|
||||
var prevContent: String? = null,
|
||||
var isUseless: Boolean = false,
|
||||
@Index var stateKey: String? = null,
|
||||
var originServerTs: Long? = null,
|
||||
@Index var sender: String? = null,
|
||||
// Can contain a serialized MatrixError
|
||||
// Can contain a serialized MatrixError
|
||||
var sendStateDetails: String? = null,
|
||||
var age: Long? = 0,
|
||||
var unsignedData: String? = null,
|
||||
@ -43,7 +41,13 @@ internal open class EventEntity(@Index var eventId: String = "",
|
||||
var decryptionResultJson: String? = null,
|
||||
var decryptionErrorCode: 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() {
|
||||
|
||||
private var sendStateStr: String = SendState.UNKNOWN.name
|
||||
@ -78,9 +82,6 @@ internal open class EventEntity(@Index var eventId: String = "",
|
||||
?.canBeProcessed = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current event is a thread root event
|
||||
*/
|
||||
fun isRootThread(): Boolean = isThread && rootThreadEventId == null
|
||||
fun isThread(): Boolean = rootThreadEventId != null
|
||||
|
||||
}
|
||||
|
@ -33,11 +33,9 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken:
|
||||
val query = where(realm, roomId)
|
||||
if (prevToken != null) {
|
||||
query.equalTo(ChunkEntityFields.PREV_TOKEN, prevToken)
|
||||
query.isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
|
||||
}
|
||||
if (nextToken != null) {
|
||||
query.equalTo(ChunkEntityFields.NEXT_TOKEN, nextToken)
|
||||
query.isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
|
||||
}
|
||||
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? {
|
||||
return where(realm, roomId)
|
||||
.equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
|
||||
.isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
|
||||
.findFirst()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds: List<String>): RealmResults<ChunkEntity> {
|
||||
return realm.where<ChunkEntity>()
|
||||
.`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, eventIds.toTypedArray())
|
||||
.isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
|
||||
.findAll()
|
||||
}
|
||||
|
||||
@ -72,16 +70,3 @@ internal fun ChunkEntity.Companion.create(
|
||||
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 {
|
||||
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.timeline.TimelineEventFilters
|
||||
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.TimelineEventEntity
|
||||
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> {
|
||||
return realm.where<TimelineEventEntity>()
|
||||
@ -59,6 +61,7 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
|
||||
filters: TimelineEventFilters = TimelineEventFilters()): TimelineEventEntity? {
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null
|
||||
val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterEvents(filters)
|
||||
|
||||
val liveEvents = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.where()?.filterEvents(filters)
|
||||
val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) {
|
||||
sendingTimelineEvents
|
||||
@ -100,6 +103,7 @@ internal fun RealmQuery<TimelineEventEntity>.filterEvents(filters: TimelineEvent
|
||||
if (filters.filterRedacted) {
|
||||
not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -101,7 +101,7 @@ internal class DefaultTimeline(
|
||||
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
|
||||
private val backwardsState = AtomicReference(TimelineState())
|
||||
private val forwardsState = AtomicReference(TimelineState())
|
||||
|
||||
private var isFromThreadTimeline = false
|
||||
override val timelineID = UUID.randomUUID().toString()
|
||||
|
||||
override val isLive
|
||||
@ -143,8 +143,9 @@ internal class DefaultTimeline(
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
override fun start(rootThreadEventId: String?) {
|
||||
if (isStarted.compareAndSet(false, true)) {
|
||||
isFromThreadTimeline = rootThreadEventId != null
|
||||
Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
|
||||
timelineInput.listeners.add(this)
|
||||
BACKGROUND_HANDLER.post {
|
||||
@ -163,7 +164,13 @@ internal class DefaultTimeline(
|
||||
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)
|
||||
handleInitialLoad()
|
||||
loadRoomMembersTask
|
||||
@ -313,16 +320,18 @@ internal class DefaultTimeline(
|
||||
val firstCacheEvent = results.firstOrNull()
|
||||
val chunkEntity = getLiveChunk()
|
||||
|
||||
|
||||
updateState(Timeline.Direction.FORWARDS) {
|
||||
it.copy(
|
||||
hasMoreInCache = !builtEventsIdMap.containsKey(firstCacheEvent?.eventId),
|
||||
hasReachedEnd = chunkEntity?.isLastForward ?: false
|
||||
hasMoreInCache = !builtEventsIdMap.containsKey(firstCacheEvent?.eventId), // what is in DB
|
||||
hasReachedEnd = if (isFromThreadTimeline) true else chunkEntity?.isLastForward ?: false // if you neeed fetch more
|
||||
)
|
||||
}
|
||||
updateState(Timeline.Direction.BACKWARDS) {
|
||||
|
||||
it.copy(
|
||||
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
|
||||
*/
|
||||
private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
|
||||
|
||||
val currentChunk = getLiveChunk()
|
||||
val token = if (direction == Timeline.Direction.BACKWARDS) currentChunk?.prevToken else currentChunk?.nextToken
|
||||
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.room.RoomAPI
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
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 io.realm.Realm
|
||||
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.isThread
|
||||
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.addTimelineEvent
|
||||
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.model.ChunkEntity
|
||||
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.RoomEntity
|
||||
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.findAllIncludingEvents
|
||||
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.where
|
||||
import org.matrix.android.sdk.internal.database.query.whereRootThreadEventId
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryEventsHelper
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return if (receivedChunk.events.isEmpty()) {
|
||||
if (receivedChunk.hasMore()) {
|
||||
Result.SHOULD_FETCH_MORE
|
||||
@ -210,6 +212,8 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
|
||||
}
|
||||
}
|
||||
val eventIds = ArrayList<String>(eventList.size)
|
||||
|
||||
val optimizedThreadSummaryMap = hashMapOf<String,EventEntity>()
|
||||
eventList.forEach { event ->
|
||||
if (event.eventId == null || event.senderId == null) {
|
||||
return@forEach
|
||||
@ -226,16 +230,18 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
|
||||
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)
|
||||
|
||||
eventEntity.rootThreadEventId?.let {
|
||||
// This is a thread event
|
||||
optimizedThreadSummaryMap[it] = eventEntity
|
||||
} ?: run {
|
||||
// This is a normal event or a root thread one
|
||||
optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
|
||||
}
|
||||
|
||||
addTimelineEventToChunk(
|
||||
realm = realm,
|
||||
roomId = roomId,
|
||||
eventEntity = eventEntity,
|
||||
currentChunk = currentChunk,
|
||||
direction = direction,
|
||||
roomMemberContentsByUser = roomMemberContentsByUser)
|
||||
}
|
||||
|
||||
// Find all the chunks which contain at least one event from the list of eventIds
|
||||
val chunks = ChunkEntity.findAllIncludingEvents(realm, 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 ||
|
||||
(chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS)
|
||||
if (shouldUpdateSummary) {
|
||||
// TODO maybe add support to view latest thread message
|
||||
roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
|
||||
}
|
||||
if (currentChunk.isValid) {
|
||||
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
|
||||
* to a specific chunk
|
||||
*/
|
||||
private fun addTimelineEventToChunk(realm: Realm,
|
||||
roomId: String,
|
||||
eventEntity: EventEntity,
|
||||
currentChunk: ChunkEntity,
|
||||
direction: PaginationDirection,
|
||||
roomMemberContentsByUser: Map<String, RoomMemberContent?>) {
|
||||
val rootThreadEventId = eventEntity.rootThreadEventId
|
||||
if (eventEntity.isThread && rootThreadEventId != null) {
|
||||
val threadChunk = getOrCreateThreadChunk(realm, roomId, rootThreadEventId)
|
||||
threadChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
|
||||
markEventAsRootEvent(realm, rootThreadEventId)
|
||||
if (threadChunk.isValid)
|
||||
RoomEntity.where(realm, roomId).findFirst()?.addIfNecessary(threadChunk)
|
||||
} else {
|
||||
currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
|
||||
}
|
||||
}
|
||||
// /**
|
||||
// * Mark or update the thread root event accordingly. If the Threading is disabled
|
||||
// * no action is done
|
||||
// */
|
||||
// private fun updateRootThreadEventIfNeeded(realm: Realm, eventEntity: EventEntity) {
|
||||
//
|
||||
// if (!BuildConfig.THREADING_ENABLED) return
|
||||
//
|
||||
// val rootThreadEventId = eventEntity.rootThreadEventId
|
||||
//
|
||||
// if (eventEntity.isThread && rootThreadEventId != null) {
|
||||
// markEventAsRootEvent(realm, rootThreadEventId)
|
||||
// } else {
|
||||
// markAsRootEventIfNeeded(realm, eventEntity.eventId)
|
||||
// }
|
||||
// }
|
||||
|
||||
private fun markEventAsRootEvent(realm: Realm, rootThreadEventId: String) {
|
||||
val rootThreadEvent = EventEntity
|
||||
.where(realm, rootThreadEventId)
|
||||
.equalTo(EventEntityFields.IS_THREAD, false).findFirst() ?: return
|
||||
rootThreadEvent.isThread = true
|
||||
}
|
||||
// /**
|
||||
// * Finds the event with rootThreadEventId and marks it as a root thread
|
||||
// */
|
||||
// private fun markEventAsRootEvent(realm: Realm, rootThreadEventId: String) {
|
||||
// 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
|
||||
*/
|
||||
private fun getOrCreateThreadChunk(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity {
|
||||
return ChunkEntity.findThreadChunkOfRoom(realm, roomId, rootThreadEventId)
|
||||
?: realm.createObject<ChunkEntity>().apply {
|
||||
this.rootThreadEventId = rootThreadEventId
|
||||
}
|
||||
}
|
||||
// /**
|
||||
// * 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
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
@ -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.events.model.Event
|
||||
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.initsync.InitSyncStep
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
|
||||
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.LazyRoomSyncEphemeral
|
||||
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.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.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import org.matrix.android.sdk.internal.database.helper.addIfNecessary
|
||||
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
|
||||
import org.matrix.android.sdk.internal.database.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.toEntity
|
||||
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.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.RoomEntity
|
||||
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.deleteOnCascade
|
||||
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
|
||||
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.findThreadChunkOfRoom
|
||||
import org.matrix.android.sdk.internal.database.query.getOrCreate
|
||||
import org.matrix.android.sdk.internal.database.query.getOrNull
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
@ -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.room.membership.RoomChangeMembershipStateDataSource
|
||||
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.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.typing.TypingEventContent
|
||||
import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy
|
||||
@ -357,11 +346,14 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||
syncLocalTimestampMillis: Long,
|
||||
aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
|
||||
val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
|
||||
|
||||
val chunkEntity = if (!isLimited && lastChunk != null) {
|
||||
// There are no more events to fetch
|
||||
lastChunk
|
||||
} else {
|
||||
realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
|
||||
}
|
||||
|
||||
// Only one chunk has isLastForward set to true
|
||||
lastChunk?.isLastForward = false
|
||||
chunkEntity.isLastForward = true
|
||||
@ -369,21 +361,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||
val eventIds = ArrayList<String>(eventList.size)
|
||||
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
|
||||
|
||||
/////////////////////
|
||||
// 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")
|
||||
/////////////////////
|
||||
val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
|
||||
for (event in eventList) {
|
||||
if (event.eventId == null || event.senderId == null || event.type == null) {
|
||||
continue
|
||||
@ -413,15 +391,14 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||
rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
|
||||
}
|
||||
|
||||
Timber.i("------> [RoomSyncHandler] Add TimelineEvent to chunkEntity event[${event.eventId}] ${if (event.isThread()) "is Thread" else ""}")
|
||||
|
||||
addTimelineEventToChunk(
|
||||
realm = realm,
|
||||
roomId = roomId,
|
||||
eventEntity = eventEntity,
|
||||
chunkEntity = chunkEntity,
|
||||
roomEntity = roomEntity,
|
||||
roomMemberContentsByUser = roomMemberContentsByUser)
|
||||
chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
|
||||
eventEntity.rootThreadEventId?.let {
|
||||
// This is a thread event
|
||||
optimizedThreadSummaryMap[it] = eventEntity
|
||||
} ?: run {
|
||||
// This is a normal event or a root thread one
|
||||
optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
|
||||
}
|
||||
|
||||
// Give info to crypto module
|
||||
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
|
||||
timelineInput.onNewTimelineEvents(roomId = roomId, eventIds = eventIds)
|
||||
|
||||
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) {
|
||||
try {
|
||||
// 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.
|
||||
buildConfigField "Boolean", "handleCallAssertedIdentityEvents", "false"
|
||||
|
||||
// Indicates whether or not threading support is enabled
|
||||
buildConfigField "Boolean", "THREADING_ENABLED", "${isThreadingEnabled}"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// Keep abiFilter for the universalApk
|
||||
|
@ -359,7 +359,6 @@ class RoomDetailFragment @Inject constructor(
|
||||
} else {
|
||||
setupToolbar(views.roomToolbar)
|
||||
}
|
||||
setupThreadIfNeeded()
|
||||
setupRecyclerView()
|
||||
setupComposer()
|
||||
setupNotificationView()
|
||||
@ -1194,12 +1193,6 @@ class RoomDetailFragment @Inject constructor(
|
||||
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun setupThreadIfNeeded(){
|
||||
getRootThreadEventId()?.let{
|
||||
textComposerViewModel.handle(TextComposerAction.EnterReplyInThreadTimeline(it))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
timelineEventController.callback = this
|
||||
timelineEventController.timeline = roomDetailViewModel.timeline
|
||||
@ -1762,7 +1755,7 @@ class RoomDetailFragment @Inject constructor(
|
||||
this.view?.hideKeyboard()
|
||||
|
||||
MessageActionsBottomSheet
|
||||
.newInstance(roomId, informationData)
|
||||
.newInstance(roomId, informationData, isThreadTimeLine())
|
||||
.show(requireActivity().supportFragmentManager, "MESSAGE_CONTEXTUAL_ACTIONS")
|
||||
|
||||
return true
|
||||
|
@ -160,7 +160,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
init {
|
||||
timeline.start()
|
||||
timeline.start(initialState.rootThreadEventId)
|
||||
timeline.addListener(this)
|
||||
observeRoomSummary()
|
||||
observeMembershipChanges()
|
||||
@ -1094,6 +1094,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
|
||||
timelineEvents.tryEmit(snapshot)
|
||||
|
||||
// PreviewUrl
|
||||
|
@ -73,7 +73,8 @@ data class RoomDetailViewState(
|
||||
roomId = args.roomId,
|
||||
eventId = args.eventId,
|
||||
// Also highlight the target event, if any
|
||||
highlightedEventId = args.eventId
|
||||
highlightedEventId = args.eventId,
|
||||
rootThreadEventId = args.roomThreadDetailArgs?.eventId
|
||||
)
|
||||
|
||||
fun isWebRTCCallOptionAvailable() = (asyncRoomSummary.invoke()?.joinedMembersCount ?: 0) <= 2
|
||||
|
@ -89,8 +89,6 @@ class TextComposerViewModel @AssistedInject constructor(
|
||||
is TextComposerAction.UserIsTyping -> handleUserIsTyping(action)
|
||||
is TextComposerAction.OnTextChanged -> handleOnTextChanged(action)
|
||||
is TextComposerAction.OnVoiceRecordingStateChanged -> handleOnVoiceRecordingStateChanged(action)
|
||||
is TextComposerAction.EnterReplyInThreadTimeline -> handleEnterReplyInThreadTimeline(action)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,10 +96,6 @@ class TextComposerViewModel @AssistedInject constructor(
|
||||
copy(isVoiceRecording = action.isRecording)
|
||||
}
|
||||
|
||||
private fun handleEnterReplyInThreadTimeline(action: TextComposerAction.EnterReplyInThreadTimeline) = setState {
|
||||
copy(rootThreadEventId = action.rootThreadEventId)
|
||||
}
|
||||
|
||||
private fun handleOnTextChanged(action: TextComposerAction.OnTextChanged) {
|
||||
setState {
|
||||
// Makes sure currentComposerText is upToDate when accessing further setState
|
||||
|
@ -53,7 +53,9 @@ data class TextComposerViewState(
|
||||
val isComposerVisible: Boolean
|
||||
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
|
||||
}
|
||||
|
@ -93,14 +93,16 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
val unreadState: UnreadState = UnreadState.Unknown,
|
||||
val highlightedEventId: String? = null,
|
||||
val jitsiState: JitsiState = JitsiState(),
|
||||
val roomSummary: RoomSummary? = null
|
||||
val roomSummary: RoomSummary? = null,
|
||||
val rootThreadEventId: String? = null
|
||||
) {
|
||||
|
||||
constructor(state: RoomDetailViewState) : this(
|
||||
unreadState = state.unreadState,
|
||||
highlightedEventId = state.highlightedEventId,
|
||||
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.
|
||||
val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId
|
||||
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) {
|
||||
modelCache[prevDisplayableEventIndex] = null
|
||||
@ -319,6 +321,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
}
|
||||
|
||||
private fun submitSnapshot(newSnapshot: List<TimelineEvent>) {
|
||||
// Update is triggered on any DB change
|
||||
backgroundHandler.post {
|
||||
inSubmitList = true
|
||||
val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot)
|
||||
@ -367,7 +370,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
val nextEvent = currentSnapshot.nextOrNull(position)
|
||||
val prevEvent = currentSnapshot.prevOrNull(position)
|
||||
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
|
||||
if (modelCache[position] == null || modelCache[position]?.isCacheable == false) {
|
||||
@ -449,7 +452,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
return null
|
||||
}
|
||||
// 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
|
||||
}
|
||||
// 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 {
|
||||
it.user.userId != session.myUserId
|
||||
}
|
||||
if (timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId)) {
|
||||
if (timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId, partialState.rootThreadEventId)) {
|
||||
lastShownEventId = event.eventId
|
||||
}
|
||||
if (lastShownEventId == null) {
|
||||
|
@ -49,10 +49,15 @@ data class MessageActionState(
|
||||
// For actions
|
||||
val actions: List<EventSharedAction> = emptyList(),
|
||||
val expendedReportContentMenu: Boolean = false,
|
||||
val actionPermissions: ActionPermissions = ActionPermissions()
|
||||
val actionPermissions: ActionPermissions = ActionPermissions(),
|
||||
val isFromThreadTimeline: Boolean = false
|
||||
) : 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() ?: ""
|
||||
|
||||
|
@ -97,13 +97,14 @@ class MessageActionsBottomSheet :
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(roomId: String, informationData: MessageInformationData): MessageActionsBottomSheet {
|
||||
fun newInstance(roomId: String, informationData: MessageInformationData, isFromThreadTimeline: Boolean): MessageActionsBottomSheet {
|
||||
return MessageActionsBottomSheet().apply {
|
||||
setArguments(
|
||||
TimelineEventFragmentArgs(
|
||||
informationData.eventId,
|
||||
roomId,
|
||||
informationData
|
||||
informationData,
|
||||
isFromThreadTimeline
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import dagger.Lazy
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.core.extensions.canReact
|
||||
@ -326,7 +327,6 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
||||
add(EventSharedAction.Reply(eventId))
|
||||
}
|
||||
|
||||
// *** Testing Threads ****
|
||||
if (canReplyInThread(timelineEvent, messageContent, actionPermissions)) {
|
||||
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
|
||||
if (!BuildConfig.THREADING_ENABLED) return false
|
||||
if (initialState.isFromThreadTimeline) return false
|
||||
if (event.root.getClearType() != EventType.MESSAGE) return false
|
||||
if (!actionPermissions.canSendMessage) return false
|
||||
return when (messageContent?.msgType) {
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
MessageType.MSGTYPE_NOTICE,
|
||||
MessageType.MSGTYPE_EMOTE,
|
||||
MessageType.MSGTYPE_IMAGE,
|
||||
MessageType.MSGTYPE_VIDEO,
|
||||
MessageType.MSGTYPE_AUDIO,
|
||||
MessageType.MSGTYPE_FILE -> true
|
||||
MessageType.MSGTYPE_TEXT -> true
|
||||
// MessageType.MSGTYPE_NOTICE,
|
||||
// MessageType.MSGTYPE_EMOTE,
|
||||
// MessageType.MSGTYPE_IMAGE,
|
||||
// MessageType.MSGTYPE_VIDEO,
|
||||
// MessageType.MSGTYPE_AUDIO,
|
||||
// MessageType.MSGTYPE_FILE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
@ -24,5 +24,6 @@ import kotlinx.parcelize.Parcelize
|
||||
data class TimelineEventFragmentArgs(
|
||||
val eventId: String,
|
||||
val roomId: String,
|
||||
val informationData: MessageInformationData
|
||||
val informationData: MessageInformationData,
|
||||
val isFromThreadTimeline: Boolean = false
|
||||
) : Parcelable
|
||||
|
@ -83,7 +83,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
||||
eventIdToHighlight: String?,
|
||||
requestModelBuild: () -> Unit,
|
||||
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()) {
|
||||
null
|
||||
} else {
|
||||
|
@ -149,7 +149,7 @@ class MessageItemFactory @Inject constructor(
|
||||
// This is an edit event, we should display it when debugging as a notice event
|
||||
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 ev = all.toModel<Event>()
|
||||
|
@ -42,8 +42,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*> {
|
||||
val event = params.event
|
||||
val computedModel = try {
|
||||
if (!timelineEventVisibilityHelper.shouldShowEvent(event, params.highlightedEventId)) {
|
||||
return buildEmptyItem(event, params.prevEvent, params.highlightedEventId)
|
||||
if (!timelineEventVisibilityHelper.shouldShowEvent(event, params.highlightedEventId, params.rootThreadEventId)) {
|
||||
return buildEmptyItem(event, params.prevEvent, params.highlightedEventId, params.rootThreadEventId)
|
||||
}
|
||||
when (event.root.getClearType()) {
|
||||
// Message itemsX
|
||||
@ -109,11 +109,11 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||
Timber.e(throwable, "failed to create message item")
|
||||
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 {
|
||||
val isNotBlank = prevEvent == null || timelineEventVisibilityHelper.shouldShowEvent(prevEvent, highlightedEventId)
|
||||
private fun buildEmptyItem(timelineEvent: TimelineEvent, prevEvent: TimelineEvent?, highlightedEventId: String?, rootThreadEventId: String?): TimelineEmptyItem {
|
||||
val isNotBlank = prevEvent == null || timelineEventVisibilityHelper.shouldShowEvent(prevEvent, highlightedEventId, rootThreadEventId)
|
||||
return TimelineEmptyItem_()
|
||||
.id(timelineEvent.localId)
|
||||
.eventId(timelineEvent.eventId)
|
||||
|
@ -34,5 +34,8 @@ data class TimelineItemFactoryParams(
|
||||
val highlightedEventId: String?
|
||||
get() = partialState.highlightedEventId
|
||||
|
||||
val rootThreadEventId: String?
|
||||
get() = partialState.rootThreadEventId
|
||||
|
||||
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.item.AbsMessageItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadDetails
|
||||
import javax.inject.Inject
|
||||
|
||||
class MessageItemAttributesFactory @Inject constructor(
|
||||
@ -32,7 +33,7 @@ class MessageItemAttributesFactory @Inject constructor(
|
||||
fun create(messageContent: Any?,
|
||||
informationData: MessageInformationData,
|
||||
callback: TimelineEventController.Callback?,
|
||||
isRootThread: Boolean = false): AbsMessageItem.Attributes {
|
||||
threadDetails: ThreadDetails? = null): AbsMessageItem.Attributes {
|
||||
return AbsMessageItem.Attributes(
|
||||
avatarSize = avatarSizeProvider.avatarSize,
|
||||
informationData = informationData,
|
||||
@ -51,7 +52,7 @@ class MessageItemAttributesFactory @Inject constructor(
|
||||
avatarCallback = callback,
|
||||
readReceiptsCallback = callback,
|
||||
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.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.RelationType
|
||||
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.room.model.Membership
|
||||
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.
|
||||
*/
|
||||
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) {
|
||||
return emptyList()
|
||||
}
|
||||
@ -59,7 +62,7 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
|
||||
} else {
|
||||
nextSameDayEvents.subList(0, indexOfFirstDifferentEventType)
|
||||
}
|
||||
val filteredSameTypeEvents = sameTypeEvents.filter { shouldShowEvent(it, eventIdToHighlight) }
|
||||
val filteredSameTypeEvents = sameTypeEvents.filter { shouldShowEvent(it, eventIdToHighlight, rootThreadEventId) }
|
||||
if (filteredSameTypeEvents.size < minSize) {
|
||||
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.
|
||||
*/
|
||||
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)
|
||||
return prevSub
|
||||
.reversed()
|
||||
.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
|
||||
* @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 (userPreferencesProvider.shouldShowHiddenEvents()) {
|
||||
return true
|
||||
@ -100,15 +103,16 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
|
||||
if (!timelineEvent.isDisplayable()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.getClearType())
|
||||
}
|
||||
|
||||
private fun TimelineEvent.shouldBeHidden(): Boolean {
|
||||
private fun TimelineEvent.shouldBeHidden(rootThreadEventId: String?): Boolean {
|
||||
if (root.isRedacted() && !userPreferencesProvider.shouldShowRedactedMessages()) {
|
||||
return true
|
||||
}
|
||||
@ -120,6 +124,11 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
|
||||
if ((diff.isJoin || diff.isPart) && !userPreferencesProvider.shouldShowJoinLeaves()) 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
|
||||
}
|
||||
|
||||
|
@ -18,10 +18,12 @@ package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.view.View
|
||||
import android.view.ViewStub
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
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.room.detail.timeline.MessageColorProvider
|
||||
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
|
||||
@ -98,9 +103,20 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||
// Render send state indicator
|
||||
holder.sendStateImageView.render(attributes.informationData.sendStateDecoration)
|
||||
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) {
|
||||
attributes.avatarRenderer.clear(holder.avatarImageView)
|
||||
holder.avatarImageView.setOnClickListener(null)
|
||||
@ -118,7 +134,11 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||
val timeView by bind<TextView>(R.id.messageTimeView)
|
||||
val sendStateImageView by bind<SendStateImageView>(R.id.messageSendStateImageView)
|
||||
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,
|
||||
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
|
||||
val emojiTypeFace: Typeface? = null,
|
||||
val isRootThread: Boolean = false
|
||||
val threadDetails: ThreadDetails? = null
|
||||
) : AbsBaseMessageItem.Attributes {
|
||||
|
||||
// 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 (informationData != other.informationData) return false
|
||||
if (threadDetails != other.threadDetails) return false
|
||||
|
||||
return true
|
||||
}
|
||||
@ -155,6 +176,8 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||
override fun hashCode(): Int {
|
||||
var result = avatarSize
|
||||
result = 31 * result + informationData.hashCode()
|
||||
result = 31 * result + threadDetails.hashCode()
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ class MergedTimelines(
|
||||
secondaryTimeline.removeAllListeners()
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
override fun start(rootThreadEventId: String?) {
|
||||
mainTimeline.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_marginTop="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_toStartOf="@+id/messageTimeView"
|
||||
android:layout_toEndOf="@+id/messageStartGuideline"
|
||||
android:layout_toStartOf="@id/messageTimeView"
|
||||
android:layout_toEndOf="@id/messageStartGuideline"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="?vctr_content_primary"
|
||||
@ -200,17 +200,7 @@
|
||||
</com.google.android.flexbox.FlexboxLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/messageIsThread"
|
||||
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" />
|
||||
<include
|
||||
layout="@layout/view_thread_room_summary" />
|
||||
|
||||
</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