Add support when there no threads messages to init timeline. Init as the normal one and hide them on the app side. That is also helpful to work to load all the threads when there is no server support

This commit is contained in:
ariskotsomitopoulos 2021-12-23 17:19:36 +02:00
parent dcabaa0dab
commit f06397023a
12 changed files with 313 additions and 52 deletions

View file

@ -145,4 +145,16 @@ interface RelationService {
autoMarkdown: Boolean = false, autoMarkdown: Boolean = false,
formattedText: String? = null, formattedText: String? = null,
eventReplied: TimelineEvent? = null): Cancelable? eventReplied: TimelineEvent? = null): Cancelable?
/**
* Get all the thread replies for the specified rootThreadEventId
* The return list will contain the original root thread event and all the thread replies to that event
* Note: We will use a large limit value in order to avoid using pagination until it would be 100% ready
* from the backend
* @param rootThreadEventId the root thread eventId
*/
suspend fun fetchThreadTimeline(rootThreadEventId: String): List<Event>
} }

View file

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

View file

@ -226,7 +226,8 @@ internal interface RoomAPI {
suspend fun getRelations(@Path("roomId") roomId: String, suspend fun getRelations(@Path("roomId") roomId: String,
@Path("eventId") eventId: String, @Path("eventId") eventId: String,
@Path("relationType") relationType: String, @Path("relationType") relationType: String,
@Path("eventType") eventType: String @Path("eventType") eventType: String,
@Query("limit") limit: Int?= null
): RelationsResponse ): RelationsResponse
/** /**
@ -377,14 +378,4 @@ internal interface RoomAPI {
suspend fun getRoomSummary(@Path("roomIdOrAlias") roomidOrAlias: String, suspend fun getRoomSummary(@Path("roomIdOrAlias") roomidOrAlias: String,
@Query("via") viaServers: List<String>?): RoomStrippedState @Query("via") viaServers: List<String>?): RoomStrippedState
// TODO add doc
/**
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/messages")
suspend fun getRoomThreadMessages(@Path("roomId") roomId: String,
@Query("from") from: String,
@Query("dir") dir: String,
@Query("limit") limit: Int,
@Query("filter") filter: String?
): PaginationResponse
} }

View file

@ -74,6 +74,8 @@ import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickR
import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask
import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask
import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask
import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportContentTask import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportContentTask
import org.matrix.android.sdk.internal.session.room.reporting.ReportContentTask import org.matrix.android.sdk.internal.session.room.reporting.ReportContentTask
import org.matrix.android.sdk.internal.session.room.state.DefaultSendStateTask import org.matrix.android.sdk.internal.session.room.state.DefaultSendStateTask
@ -256,4 +258,7 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindGetRoomSummaryTask(task: DefaultGetRoomSummaryTask): GetRoomSummaryTask abstract fun bindGetRoomSummaryTask(task: DefaultGetRoomSummaryTask): GetRoomSummaryTask
@Binds
abstract fun bindFetchThreadTimelineTask(task: DefaultFetchThreadTimelineTask): FetchThreadTimelineTask
} }

View file

@ -21,26 +21,48 @@ import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.realm.Realm
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
import org.matrix.android.sdk.api.session.room.model.relation.RelationService import org.matrix.android.sdk.api.session.room.model.relation.RelationService
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.api.util.NoOpCancellable
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider 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.addTimelineEvent
import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.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.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.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.findIncludingEvent
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.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.util.fetchCopyMap import org.matrix.android.sdk.internal.util.fetchCopyMap
import timber.log.Timber import timber.log.Timber
@ -50,9 +72,12 @@ internal class DefaultRelationService @AssistedInject constructor(
private val eventSenderProcessor: EventSenderProcessor, private val eventSenderProcessor: EventSenderProcessor,
private val eventFactory: LocalEchoEventFactory, private val eventFactory: LocalEchoEventFactory,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
private val cryptoService: DefaultCryptoService,
private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
private val fetchEditHistoryTask: FetchEditHistoryTask, private val fetchEditHistoryTask: FetchEditHistoryTask,
private val fetchThreadTimelineTask: FetchThreadTimelineTask,
private val timelineEventMapper: TimelineEventMapper, private val timelineEventMapper: TimelineEventMapper,
@UserId private val userId: String,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor) : private val taskExecutor: TaskExecutor) :
RelationService { RelationService {
@ -192,7 +217,77 @@ internal class DefaultRelationService @AssistedInject constructor(
saveLocalEcho(it) saveLocalEcho(it)
} }
} }
return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId)) return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
}
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
}
}
}
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
} }
/** /**

View file

@ -0,0 +1,55 @@
/*
* 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.session.room.relation.threads
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.internal.crypto.CryptoSessionInfoProvider
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params, List<Event>> {
data class Params(
val roomId: String,
val rootThreadEventId: String
)
}
internal class DefaultFetchThreadTimelineTask @Inject constructor(
private val roomAPI: RoomAPI,
private val globalErrorReceiver: GlobalErrorReceiver,
private val cryptoSessionInfoProvider: CryptoSessionInfoProvider
) : FetchThreadTimelineTask {
override suspend fun execute(params: FetchThreadTimelineTask.Params): List<Event> {
val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
val response = executeRequest(globalErrorReceiver) {
roomAPI.getRelations(
roomId = params.roomId,
eventId = params.rootThreadEventId,
relationType = RelationType.IO_THREAD,
eventType = if (isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE,
limit = 2000
)
}
return response.chunks + listOfNotNull(response.originalEvent)
}
}

View file

@ -53,6 +53,7 @@ import org.matrix.android.sdk.internal.util.Debouncer
import org.matrix.android.sdk.internal.util.createBackgroundHandler import org.matrix.android.sdk.internal.util.createBackgroundHandler
import org.matrix.android.sdk.internal.util.createUIHandler import org.matrix.android.sdk.internal.util.createUIHandler
import timber.log.Timber import timber.log.Timber
import java.lang.Thread.sleep
import java.util.Collections import java.util.Collections
import java.util.UUID import java.util.UUID
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
@ -107,6 +108,7 @@ internal class DefaultTimeline(
private val backwardsState = AtomicReference(TimelineState()) private val backwardsState = AtomicReference(TimelineState())
private val forwardsState = AtomicReference(TimelineState()) private val forwardsState = AtomicReference(TimelineState())
private var isFromThreadTimeline = false private var isFromThreadTimeline = false
private var rootThreadEventId: String? = null
override val timelineID = UUID.randomUUID().toString() override val timelineID = UUID.randomUUID().toString()
override val isLive override val isLive
@ -151,9 +153,11 @@ internal class DefaultTimeline(
override fun start(rootThreadEventId: String?) { override fun start(rootThreadEventId: String?) {
if (isStarted.compareAndSet(false, true)) { if (isStarted.compareAndSet(false, true)) {
isFromThreadTimeline = rootThreadEventId != null isFromThreadTimeline = rootThreadEventId != null
this@DefaultTimeline.rootThreadEventId = rootThreadEventId
Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId") Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
timelineInput.listeners.add(this) timelineInput.listeners.add(this)
BACKGROUND_HANDLER.post { BACKGROUND_HANDLER.post {
eventDecryptor.start() eventDecryptor.start()
val realm = Realm.getInstance(realmConfiguration) val realm = Realm.getInstance(realmConfiguration)
backgroundRealm.set(realm) backgroundRealm.set(realm)
@ -170,9 +174,10 @@ internal class DefaultTimeline(
} }
timelineEvents = rootThreadEventId?.let { timelineEvents = rootThreadEventId?.let {
TimelineEventEntity val threadTimelineEvents = TimelineEventEntity
.whereRoomId(realm, roomId = roomId) .whereRoomId(realm, roomId = roomId)
.equalTo(TimelineEventEntityFields.CHUNK.IS_LAST_FORWARD, true) .equalTo(TimelineEventEntityFields.CHUNK.IS_LAST_FORWARD, true)
// .`in`("${TimelineEventEntityFields.CHUNK.TIMELINE_EVENTS}.${TimelineEventEntityFields.EVENT_ID}", arrayOf(it))
.beginGroup() .beginGroup()
.equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, it) .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, it)
.or() .or()
@ -180,7 +185,15 @@ internal class DefaultTimeline(
.endGroup() .endGroup()
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
.findAll() .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() } ?: buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
if (isFromThreadTimeline)
Timber.i("----> timelineEvents.size: ${timelineEvents.size}")
timelineEvents.addChangeListener(eventsChangeListener) timelineEvents.addChangeListener(eventsChangeListener)
handleInitialLoad() handleInitialLoad()
@ -330,17 +343,19 @@ internal class DefaultTimeline(
val lastCacheEvent = results.lastOrNull() val lastCacheEvent = results.lastOrNull()
val firstCacheEvent = results.firstOrNull() val firstCacheEvent = results.firstOrNull()
val chunkEntity = getLiveChunk() val chunkEntity = getLiveChunk()
if (isFromThreadTimeline)
Timber.i("----> results.size: ${results.size} | contains root thread ${results.map { it.eventId }.contains(rootThreadEventId)}")
updateState(Timeline.Direction.FORWARDS) { updateState(Timeline.Direction.FORWARDS) { state ->
it.copy( state.copy(
hasMoreInCache = !builtEventsIdMap.containsKey(firstCacheEvent?.eventId), // what is in DB hasMoreInCache = !builtEventsIdMap.containsKey(firstCacheEvent?.eventId), // what is in DB
hasReachedEnd = if (isFromThreadTimeline) true else chunkEntity?.isLastForward ?: false // if you neeed fetch more hasReachedEnd = if (isFromThreadTimeline) true else chunkEntity?.isLastForward ?: false // if you neeed fetch more
) )
} }
updateState(Timeline.Direction.BACKWARDS) { updateState(Timeline.Direction.BACKWARDS) { state ->
it.copy( state.copy(
hasMoreInCache = !builtEventsIdMap.containsKey(lastCacheEvent?.eventId), hasMoreInCache = !builtEventsIdMap.containsKey(lastCacheEvent?.eventId),
hasReachedEnd = if (isFromThreadTimeline) true else chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE hasReachedEnd = if (isFromThreadTimeline && results.map { it.eventId }.contains(rootThreadEventId)) true else (chunkEntity?.isLastBackward ?: false || lastCacheEvent?.root?.type == EventType.STATE_ROOM_CREATE)
) )
} }
} }
@ -640,7 +655,7 @@ internal class DefaultTimeline(
}.map { }.map {
EventMapper.map(it) EventMapper.map(it)
} }
threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList) threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList)
} }
private fun buildTimelineEvent(eventEntity: TimelineEventEntity): TimelineEvent { private fun buildTimelineEvent(eventEntity: TimelineEventEntity): TimelineEvent {

View file

@ -180,6 +180,15 @@ class RoomDetailViewModel @AssistedInject constructor(
if (OutboundSessionKeySharingStrategy.WhenEnteringRoom == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) { if (OutboundSessionKeySharingStrategy.WhenEnteringRoom == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) {
prepareForEncryption() prepareForEncryption()
} }
// Threads
initThreads()
}
/**
* Threads specific initialization
*/
private fun initThreads() {
markThreadTimelineAsReadLocal() markThreadTimelineAsReadLocal()
observeLocalThreadNotifications() observeLocalThreadNotifications()
} }
@ -269,6 +278,18 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
} }
/**
* Mark the thread as read, while the user navigated within the thread
* This is a local implementation has nothing to do with APIs
*/
private fun markThreadTimelineAsReadLocal() {
initialState.rootThreadEventId?.let {
session.coroutineScope.launch {
room.markThreadAsRead(it)
}
}
}
/** /**
* Observe local unread threads * Observe local unread threads
*/ */
@ -287,6 +308,17 @@ 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 getOtherUserIds() = room.roomSummary()?.otherMemberIds
fun getRoomSummary() = room.roomSummary() fun getRoomSummary() = room.roomSummary()
@ -1076,18 +1108,6 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
} }
/**
* Mark the thread as read, while the user navigated within the thread
* This is a local implementation has nothing to do with APIs
*/
private fun markThreadTimelineAsReadLocal() {
initialState.rootThreadEventId?.let {
session.coroutineScope.launch {
room.markThreadAsRead(it)
}
}
}
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
viewModelScope.launch { viewModelScope.launch {
// tryEmit doesn't work with SharedFlow without cache // tryEmit doesn't work with SharedFlow without cache
@ -1125,6 +1145,8 @@ class RoomDetailViewModel @AssistedInject constructor(
chatEffectManager.delegate = null chatEffectManager.delegate = null
chatEffectManager.dispose() chatEffectManager.dispose()
callManager.removeProtocolsCheckerListener(this) callManager.removeProtocolsCheckerListener(this)
// we should also mark it as read here, for the scenario that the user
// is already in the thread timeline
markThreadTimelineAsReadLocal() markThreadTimelineAsReadLocal()
super.onCleared() super.onCleared()
} }

View file

@ -200,7 +200,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
// it's sent by the same user so we are sure we have up to date information. // it's sent by the same user so we are sure we have up to date information.
val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId
val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast { val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast {
timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId, partialState.isFromThreadTimeline()) timelineEventVisibilityHelper.shouldShowEvent(
timelineEvent = it,
highlightedEventId = partialState.highlightedEventId,
isFromThreadTimeline = partialState.isFromThreadTimeline(),
rootThreadEventId = partialState.rootThreadEventId
)
} }
if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) { if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) {
modelCache[prevDisplayableEventIndex] = null modelCache[prevDisplayableEventIndex] = null
@ -377,7 +382,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val nextEvent = currentSnapshot.nextOrNull(position) val nextEvent = currentSnapshot.nextOrNull(position)
val prevEvent = currentSnapshot.prevOrNull(position) val prevEvent = currentSnapshot.prevOrNull(position)
val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull { val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId, partialState.isFromThreadTimeline()) timelineEventVisibilityHelper.shouldShowEvent(
timelineEvent = it,
highlightedEventId = partialState.highlightedEventId,
isFromThreadTimeline = partialState.isFromThreadTimeline(),
rootThreadEventId = partialState.rootThreadEventId)
} }
// Should be build if not cached or if model should be refreshed // Should be build if not cached or if model should be refreshed
if (modelCache[position] == null || modelCache[position]?.isCacheable == false) { if (modelCache[position] == null || modelCache[position]?.isCacheable == false) {
@ -459,7 +468,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
return null return null
} }
// If the event is not shown, we go to the next one // If the event is not shown, we go to the next one
if (!timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId, partialState.isFromThreadTimeline())) { if (!timelineEventVisibilityHelper.shouldShowEvent(
timelineEvent = event,
highlightedEventId = partialState.highlightedEventId,
isFromThreadTimeline = partialState.isFromThreadTimeline(),
rootThreadEventId = partialState.rootThreadEventId
)) {
continue continue
} }
// If the event is sent by us, we update the holder with the eventId and stop the search // If the event is sent by us, we update the holder with the eventId and stop the search
@ -481,7 +495,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val currentReadReceipts = ArrayList(event.readReceipts).filter { val currentReadReceipts = ArrayList(event.readReceipts).filter {
it.user.userId != session.myUserId it.user.userId != session.myUserId
} }
if (timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId, partialState.isFromThreadTimeline())) { if (timelineEventVisibilityHelper.shouldShowEvent(
timelineEvent = event,
highlightedEventId = partialState.highlightedEventId,
isFromThreadTimeline = partialState.isFromThreadTimeline(),
rootThreadEventId = partialState.rootThreadEventId)) {
lastShownEventId = event.eventId lastShownEventId = event.eventId
} }
if (lastShownEventId == null) { if (lastShownEventId == null) {

View file

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

View file

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

View file

@ -40,7 +40,13 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
* *
* @return a list of timeline events which have sequentially the same type following the next direction. * @return a list of timeline events which have sequentially the same type following the next direction.
*/ */
private fun nextSameTypeEvents(timelineEvents: List<TimelineEvent>, index: Int, minSize: Int, eventIdToHighlight: String?, isFromThreadTimeline: Boolean): List<TimelineEvent> { private fun nextSameTypeEvents(
timelineEvents: List<TimelineEvent>,
index: Int,
minSize: Int,
eventIdToHighlight: String?,
rootThreadEventId: String?,
isFromThreadTimeline: Boolean): List<TimelineEvent> {
if (index >= timelineEvents.size - 1) { if (index >= timelineEvents.size - 1) {
return emptyList() return emptyList()
} }
@ -62,11 +68,18 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
} else { } else {
nextSameDayEvents.subList(0, indexOfFirstDifferentEventType) nextSameDayEvents.subList(0, indexOfFirstDifferentEventType)
} }
val filteredSameTypeEvents = sameTypeEvents.filter { shouldShowEvent(it, eventIdToHighlight, isFromThreadTimeline) } val filteredSameTypeEvents = sameTypeEvents.filter {
shouldShowEvent(
timelineEvent = it,
highlightedEventId = eventIdToHighlight,
isFromThreadTimeline = isFromThreadTimeline,
rootThreadEventId = rootThreadEventId
)
}
if (filteredSameTypeEvents.size < minSize) { if (filteredSameTypeEvents.size < minSize) {
return emptyList() return emptyList()
} }
return filteredSameTypeEvents return filteredSameTypeEvents
} }
/** /**
@ -77,12 +90,12 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
* *
* @return a list of timeline events which have sequentially the same type following the prev direction. * @return a list of timeline events which have sequentially the same type following the prev direction.
*/ */
fun prevSameTypeEvents(timelineEvents: List<TimelineEvent>, index: Int, minSize: Int, eventIdToHighlight: String?, isFromThreadTimeline: Boolean): List<TimelineEvent> { fun prevSameTypeEvents(timelineEvents: List<TimelineEvent>, index: Int, minSize: Int, eventIdToHighlight: String?, rootThreadEventId: String?, isFromThreadTimeline: Boolean): List<TimelineEvent> {
val prevSub = timelineEvents.subList(0, index + 1) val prevSub = timelineEvents.subList(0, index + 1)
return prevSub return prevSub
.reversed() .reversed()
.let { .let {
nextSameTypeEvents(it, 0, minSize, eventIdToHighlight, isFromThreadTimeline) nextSameTypeEvents(it, 0, minSize, eventIdToHighlight, rootThreadEventId, isFromThreadTimeline)
} }
} }
@ -92,7 +105,12 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
* @param rootThreadEventId if this param is null it means we are in the original timeline * @param rootThreadEventId if this param is null it means we are in the original timeline
* @return true if the event should be shown in the timeline. * @return true if the event should be shown in the timeline.
*/ */
fun shouldShowEvent(timelineEvent: TimelineEvent, highlightedEventId: String?, isFromThreadTimeline: Boolean): Boolean { fun shouldShowEvent(
timelineEvent: TimelineEvent,
highlightedEventId: String?,
isFromThreadTimeline: Boolean,
rootThreadEventId: String?
): Boolean {
// If show hidden events is true we should always display something // If show hidden events is true we should always display something
if (userPreferencesProvider.shouldShowHiddenEvents()) { if (userPreferencesProvider.shouldShowHiddenEvents()) {
return true return true
@ -106,14 +124,14 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
} }
// Check for special case where we should hide the event, like redacted, relation, memberships... according to user preferences. // Check for special case where we should hide the event, like redacted, relation, memberships... according to user preferences.
return !timelineEvent.shouldBeHidden(isFromThreadTimeline) return !timelineEvent.shouldBeHidden(rootThreadEventId, isFromThreadTimeline)
} }
private fun TimelineEvent.isDisplayable(): Boolean { private fun TimelineEvent.isDisplayable(): Boolean {
return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.getClearType()) return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.getClearType())
} }
private fun TimelineEvent.shouldBeHidden(isFromThreadTimeline: Boolean): Boolean { private fun TimelineEvent.shouldBeHidden(rootThreadEventId: String?, isFromThreadTimeline: Boolean): Boolean {
if (root.isRedacted() && !userPreferencesProvider.shouldShowRedactedMessages()) { if (root.isRedacted() && !userPreferencesProvider.shouldShowRedactedMessages()) {
return true return true
} }
@ -128,10 +146,18 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
return true return true
} }
if (BuildConfig.THREADING_ENABLED && !isFromThreadTimeline && root.isThread() && root.getRootThreadEventId() != null) { if (BuildConfig.THREADING_ENABLED && !isFromThreadTimeline && root.isThread()) {
return true return true
} }
if (BuildConfig.THREADING_ENABLED && isFromThreadTimeline) {
////
return if (root.getRootThreadEventId() == rootThreadEventId) {
false
} else root.eventId != rootThreadEventId
}
return false return false
} }