1
0
mirror of https://github.com/vector-im/riotX-android synced 2025-10-06 00:02:48 +02:00

Compare commits

...

3 Commits

Author SHA1 Message Date
ariskotsomitopoulos
8ee8198bf1 Improve thread replies API fetch 2022-01-01 21:38:10 +02:00
ariskotsomitopoulos
17e17abb57 Remove timeline restart on new thread replies found 2021-12-24 18:30:09 +02:00
ariskotsomitopoulos
02a34bbc91 - Fetch all thread replies using relation api
- Enhance to also display reactions on those events
2021-12-24 18:11:53 +02:00
14 changed files with 242 additions and 89 deletions

View File

@@ -47,7 +47,7 @@ android {
testOptions {
// Comment to run on Android 12
execution 'ANDROIDX_TEST_ORCHESTRATOR'
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
buildTypes {

View File

@@ -156,5 +156,5 @@ interface RelationService {
* from the backend
* @param rootThreadEventId the root thread eventId
*/
suspend fun fetchThreadTimeline(rootThreadEventId: String): List<Event>
suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean
}

View File

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

View File

@@ -82,7 +82,7 @@ internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity,
internal fun ChunkEntity.addTimelineEvent(roomId: String,
eventEntity: EventEntity,
direction: PaginationDirection,
roomMemberContentsByUser: Map<String, RoomMemberContent?>? = null) {
roomMemberContentsByUser: Map<String, RoomMemberContent?>) {
val eventId = eventEntity.eventId
if (timelineEvents.find(eventId) != null) {
return
@@ -102,7 +102,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
?.also { it.cleanUp(eventEntity.sender) }
this.readReceipts = readReceiptsSummaryEntity
this.displayIndex = displayIndex
val roomMemberContent = roomMemberContentsByUser?.get(senderId)
val roomMemberContent = roomMemberContentsByUser[senderId]
this.senderAvatar = roomMemberContent?.avatarUrl
this.senderName = roomMemberContent?.displayName
isUniqueDisplayName = if (roomMemberContent?.displayName != null) {

View File

@@ -36,7 +36,11 @@ 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(roomId: String, realm: Realm, currentUserId: String) {
internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(roomId: String,
realm: Realm,
currentUserId: String,
shouldUpdateNotifications: Boolean = true
) {
if (!BuildConfig.THREADING_ENABLED) return
for ((rootThreadEventId, eventEntity) in this) {
@@ -54,8 +58,8 @@ internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(roomId: String
)
}
}
updateNotificationsNew(roomId, realm, currentUserId)
if(shouldUpdateNotifications)
updateNotificationsNew(roomId, realm, currentUserId)
}
/**

View File

@@ -238,56 +238,8 @@ internal class DefaultRelationService @AssistedInject constructor(
}
}
override suspend fun fetchThreadTimeline(rootThreadEventId: String): List<Event> {
val results = fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(roomId, rootThreadEventId))
var counter = 0
//
// monarchy
// .awaitTransaction { realm ->
// val chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
//
// val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
// for (event in results.reversed()) {
// if (event.eventId == null || event.senderId == null || event.type == null) {
// continue
// }
//
// // skip if event already exists
// if (EventEntity.where(realm, event.eventId).findFirst() != null) {
// counter++
// continue
// }
//
// if (event.isEncrypted()) {
// decryptIfNeeded(event, roomId)
// }
//
// val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
// val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
// if (event.stateKey != null) {
// CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
// eventId = event.eventId
// root = eventEntity
// }
// }
// chunk?.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS)
// 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
// }
// }
//
// optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
// roomId = roomId,
// realm = realm,
// currentUserId = userId)
// }
Timber.i("----> size: ${results.size} | skipped: $counter | threads: ${results.map{ it.eventId}}")
return results
override suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean {
return fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(roomId, rootThreadEventId))
}
/**

View File

@@ -15,17 +15,45 @@
*/
package org.matrix.android.sdk.internal.session.room.relation.threads
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
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.RelationType
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
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.EventAnnotationsSummaryEntity
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.getOrNull
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import timber.log.Timber
import javax.inject.Inject
internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params, List<Event>> {
internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params, Boolean> {
data class Params(
val roomId: String,
val rootThreadEventId: String
@@ -35,10 +63,13 @@ internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params
internal class DefaultFetchThreadTimelineTask @Inject constructor(
private val roomAPI: RoomAPI,
private val globalErrorReceiver: GlobalErrorReceiver,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
@SessionDatabase private val monarchy: Monarchy,
@UserId private val userId: String,
private val cryptoService: DefaultCryptoService
) : FetchThreadTimelineTask {
override suspend fun execute(params: FetchThreadTimelineTask.Params): List<Event> {
override suspend fun execute(params: FetchThreadTimelineTask.Params): Boolean {
val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
val response = executeRequest(globalErrorReceiver) {
roomAPI.getRelations(
@@ -49,7 +80,130 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
limit = 2000
)
}
val threadList = response.chunks + listOfNotNull(response.originalEvent)
return response.chunks + listOfNotNull(response.originalEvent)
return storeNewEventsIfNeeded(threadList, params.roomId)
}
/**
* Store new events if they are not already received, and returns weather or not,
* a timeline update should be made
* @param threadList is the list containing the thread replies
* @param roomId the roomId of the the thread
* @return
*/
private suspend fun storeNewEventsIfNeeded(threadList: List<Event>, roomId: String): Boolean {
var eventsSkipped = 0
monarchy
.awaitTransaction { realm ->
val chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
for (event in threadList.reversed()) {
if (event.eventId == null || event.senderId == null || event.type == null) {
eventsSkipped++
continue
}
if (EventEntity.where(realm, event.eventId).findFirst() != null) {
// Skip if event already exists
eventsSkipped++
continue
}
if (event.isEncrypted()) {
// Decrypt events that will be stored
decryptIfNeeded(event, roomId)
}
handleReaction(realm, event, roomId)
val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
// Sender info
roomMemberContentsByUser.getOrPut(event.senderId) {
// If we don't have any new state on this user, get it from db
val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
}
chunk?.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
}
}
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
roomId = roomId,
realm = realm,
currentUserId = userId,
shouldUpdateNotifications = false
)
}
Timber.i("----> size: ${threadList.size} | skipped: $eventsSkipped | threads: ${threadList.map { it.eventId }}")
return eventsSkipped == threadList.size
}
/**
* Invoke the event decryption mechanism for a specific event
*/
private fun decryptIfNeeded(event: Event, roomId: String) {
try {
// Event from sync does not have roomId, so add it to the event first
val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
event.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
if (e is MXCryptoError.Base) {
event.mCryptoError = e.errorType
event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
}
}
}
private fun handleReaction(realm: Realm,
event: Event,
roomId: String) {
val unsignedData = event.unsignedData ?: return
val relatedEventId = event.eventId ?: return
unsignedData.relations?.annotations?.chunk?.forEach { relationChunk ->
if (relationChunk.type == EventType.REACTION) {
val reaction = relationChunk.key
Timber.i("----> Annotation found in ${event.eventId} ${relationChunk.key} ")
val eventSummary = EventAnnotationsSummaryEntity.getOrCreate(realm, roomId, relatedEventId)
var sum = eventSummary.reactionsSummary.find { it.key == reaction }
if (sum == null) {
sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
sum.key = reaction
sum.firstTimestamp = event.originServerTs ?: 0
Timber.v("Adding synced reaction $reaction")
sum.count = 1
// reactionEventId not included in the /relations API
// sum.sourceEvents.add(reactionEventId)
eventSummary.reactionsSummary.add(sum)
} else {
sum.count += 1
}
}
}
}
}

View File

@@ -45,6 +45,7 @@ import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendState
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
import org.matrix.android.sdk.internal.task.TaskExecutor
@@ -78,6 +79,7 @@ internal class DefaultTimeline(
private val realmSessionProvider: RealmSessionProvider,
private val loadRoomMembersTask: LoadRoomMembersTask,
private val threadsAwarenessHandler: ThreadsAwarenessHandler,
private val fetchThreadTimelineTask: FetchThreadTimelineTask,
private val readReceiptHandler: ReadReceiptHandler
) : Timeline,
TimelineInput.Listener,
@@ -130,6 +132,9 @@ internal class DefaultTimeline(
return@post
}
Timber.v("Paginate $direction of $count items")
if (isFromThreadTimeline) {
Timber.v("----> Paginate $direction of $count items")
}
val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex
val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, count)
if (shouldPostSnapshot) {
@@ -150,7 +155,7 @@ internal class DefaultTimeline(
}
}
override fun start(rootThreadEventId: String?) {
override fun start(rootThreadEventId: String?, shouldFetchThreadTimeline: Boolean) {
if (isStarted.compareAndSet(false, true)) {
isFromThreadTimeline = rootThreadEventId != null
this@DefaultTimeline.rootThreadEventId = rootThreadEventId
@@ -174,10 +179,12 @@ internal class DefaultTimeline(
}
timelineEvents = rootThreadEventId?.let {
val threadTimelineEvents = TimelineEventEntity
if (shouldFetchThreadTimeline) {
fetchThreadTimeline(it)
}
TimelineEventEntity
.whereRoomId(realm, roomId = roomId)
.equalTo(TimelineEventEntityFields.CHUNK.IS_LAST_FORWARD, true)
// .`in`("${TimelineEventEntityFields.CHUNK.TIMELINE_EVENTS}.${TimelineEventEntityFields.EVENT_ID}", arrayOf(it))
.beginGroup()
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, it)
.or()
@@ -185,12 +192,6 @@ internal class DefaultTimeline(
.endGroup()
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
.findAll()
if (threadTimelineEvents.isNullOrEmpty()) {
// When there no threads in the last forward chunk get all events and hide them
buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
} else {
threadTimelineEvents
}
} ?: buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
if (isFromThreadTimeline)
Timber.i("----> timelineEvents.size: ${timelineEvents.size}")
@@ -248,7 +249,7 @@ internal class DefaultTimeline(
override fun restartWithEventId(eventId: String?) {
dispose()
initialEventId = eventId
start()
start(rootThreadEventId, false)
postSnapshot()
}
@@ -343,8 +344,13 @@ internal class DefaultTimeline(
val lastCacheEvent = results.lastOrNull()
val firstCacheEvent = results.firstOrNull()
val chunkEntity = getLiveChunk()
if (isFromThreadTimeline)
Timber.i("----> results.size: ${results.size} | contains root thread ${results.map { it.eventId }.contains(rootThreadEventId)}")
// if (isFromThreadTimeline) {
// Timber.i("----> ----> ----> ----> ----> ----> ----> ----> ----> ")
// Timber.i("----> lastCacheEvent: ${lastCacheEvent?.eventId}")
// Timber.i("----> firstCacheEvent: ${firstCacheEvent?.eventId}")
// Timber.i("----> LOADING_STATE results.size: ${results.size} | contains root thread (END BACKWORDS) ${results.map { it.eventId }.contains(rootThreadEventId)}")
// Timber.i("----> LOADING_STATE builtEventsIdMap.size: $builtEventsIdMap |: $builtEventsIdMap")
// }
updateState(Timeline.Direction.FORWARDS) { state ->
state.copy(
@@ -355,7 +361,11 @@ internal class DefaultTimeline(
updateState(Timeline.Direction.BACKWARDS) { state ->
state.copy(
hasMoreInCache = !builtEventsIdMap.containsKey(lastCacheEvent?.eventId),
hasReachedEnd = if (isFromThreadTimeline && results.map { it.eventId }.contains(rootThreadEventId)) true else (chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE)
hasReachedEnd = if (isFromThreadTimeline && results.map { it.eventId }.contains(rootThreadEventId)) {
true
} else if (isFromThreadTimeline) {
false
} else (chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE)
)
}
}
@@ -785,4 +795,28 @@ internal class DefaultTimeline(
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
}
// Threads
private fun fetchThreadTimeline(rootThreadEventId: String) {
val params = FetchThreadTimelineTask.Params(roomId, rootThreadEventId)
cancelableBag += fetchThreadTimelineTask
.configureWith(params) {
this.callback = fetchThreadTimelineCallback()
}
.executeBy(taskExecutor)
}
private fun fetchThreadTimelineCallback(): MatrixCallback<Boolean> {
return object : MatrixCallback<Boolean> {
override fun onSuccess(data: Boolean) {
if (!data) {
Timber.i("----> New events founds for thread: $rootThreadEventId ")
postSnapshot()
}
}
override fun onFailure(failure: Throwable) {
}
}
}
}

View File

@@ -44,6 +44,8 @@ import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
import org.matrix.android.sdk.internal.session.room.relation.DefaultRelationService
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
import org.matrix.android.sdk.internal.task.TaskExecutor
@@ -63,6 +65,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
private val timelineEventMapper: TimelineEventMapper,
private val loadRoomMembersTask: LoadRoomMembersTask,
private val threadsAwarenessHandler: ThreadsAwarenessHandler,
private val fetchThreadTimelineTask: FetchThreadTimelineTask,
private val readReceiptHandler: ReadReceiptHandler
) : TimelineService {
@@ -86,6 +89,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
realmSessionProvider = realmSessionProvider,
loadRoomMembersTask = loadRoomMembersTask,
fetchThreadTimelineTask = fetchThreadTimelineTask,
threadsAwarenessHandler = threadsAwarenessHandler,
readReceiptHandler = readReceiptHandler
)

View File

@@ -308,17 +308,6 @@ class RoomDetailViewModel @AssistedInject constructor(
}
}
// /**
// * Fetch all the thread replies for the current thread
// */
// private fun fetchThreadTimeline() {
// initialState.rootThreadEventId?.let {
// viewModelScope.launch(Dispatchers.IO) {
// room.fetchThreadTimeline(it)
// }
// }
// }
fun getOtherUserIds() = room.roomSummary()?.otherMemberIds
fun getRoomSummary() = room.roomSummary()
@@ -738,6 +727,7 @@ class RoomDetailViewModel @AssistedInject constructor(
}
private fun handleLoadMore(action: RoomDetailAction.LoadMoreTimelineEvents) {
timeline.paginate(action.direction, PAGINATION_COUNT)
}

View File

@@ -451,7 +451,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
val readReceipts = receiptsByEvents[event.eventId].orEmpty()
return copy(
readReceiptsItem = readReceiptsItemFactory.create(event.eventId, readReceipts, callback),
readReceiptsItem = readReceiptsItemFactory.create(
event.eventId,
readReceipts,
callback,
partialState.isFromThreadTimeline()
),
formattedDayModel = formattedDayModel,
mergedHeaderModel = mergedHeaderModel
)

View File

@@ -26,7 +26,12 @@ import javax.inject.Inject
class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer) {
fun create(eventId: String, readReceipts: List<ReadReceipt>, callback: TimelineEventController.Callback?): ReadReceiptsItem? {
fun create(
eventId: String,
readReceipts: List<ReadReceipt>,
callback: TimelineEventController.Callback?,
isFromThreadTimeLine: Boolean
): ReadReceiptsItem? {
if (readReceipts.isEmpty()) {
return null
}
@@ -41,6 +46,7 @@ class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: Av
.eventId(eventId)
.readReceipts(readReceiptsData)
.avatarRenderer(avatarRenderer)
.shouldHideReadReceipts(isFromThreadTimeLine)
.clickListener {
callback?.onReadReceiptsClicked(readReceiptsData)
}

View File

@@ -16,6 +16,7 @@
package im.vector.app.features.home.room.detail.timeline.item
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
@@ -31,6 +32,8 @@ abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(
@EpoxyAttribute lateinit var eventId: String
@EpoxyAttribute lateinit var readReceipts: List<ReadReceiptData>
@EpoxyAttribute var shouldHideReadReceipts: Boolean = false
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var clickListener: ClickListener
@@ -42,6 +45,7 @@ abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(
super.bind(holder)
holder.readReceiptsView.onClick(clickListener)
holder.readReceiptsView.render(readReceipts, avatarRenderer)
holder.readReceiptsView.isVisible = !shouldHideReadReceipts
}
override fun unbind(holder: Holder) {

View File

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