From f06397023a8b7e5d984270a97567820bc1608ab7 Mon Sep 17 00:00:00 2001 From: ariskotsomitopoulos Date: Thu, 23 Dec 2021 17:19:36 +0200 Subject: [PATCH] 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 --- .../room/model/relation/RelationService.kt | 12 +++ .../database/helper/ChunkEntityHelper.kt | 4 +- .../sdk/internal/session/room/RoomAPI.kt | 13 +-- .../sdk/internal/session/room/RoomModule.kt | 5 + .../room/relation/DefaultRelationService.kt | 97 ++++++++++++++++++- .../threads/FetchThreadTimelineTask.kt | 55 +++++++++++ .../session/room/timeline/DefaultTimeline.kt | 29 ++++-- .../home/room/detail/RoomDetailViewModel.kt | 46 ++++++--- .../timeline/TimelineEventController.kt | 26 ++++- .../factory/MergedHeaderItemFactory.kt | 2 +- .../timeline/factory/TimelineItemFactory.kt | 32 +++++- .../helper/TimelineEventVisibilityHelper.kt | 44 +++++++-- 12 files changed, 313 insertions(+), 52 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt index a5ecfaf6e4..4f28f7dce1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt @@ -145,4 +145,16 @@ interface RelationService { autoMarkdown: Boolean = false, formattedText: String? = null, 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 } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt index b0d15ce8da..0b8c42c8cd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt @@ -82,7 +82,7 @@ internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, internal fun ChunkEntity.addTimelineEvent(roomId: String, eventEntity: EventEntity, direction: PaginationDirection, - roomMemberContentsByUser: Map) { + roomMemberContentsByUser: Map? = null) { 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[senderId] + val roomMemberContent = roomMemberContentsByUser?.get(senderId) this.senderAvatar = roomMemberContent?.avatarUrl this.senderName = roomMemberContent?.displayName isUniqueDisplayName = if (roomMemberContent?.displayName != null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 0017cdd917..2dd1871ac0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -226,7 +226,8 @@ internal interface RoomAPI { suspend fun getRelations(@Path("roomId") roomId: String, @Path("eventId") eventId: String, @Path("relationType") relationType: String, - @Path("eventType") eventType: String + @Path("eventType") eventType: String, + @Query("limit") limit: Int?= null ): RelationsResponse /** @@ -377,14 +378,4 @@ internal interface RoomAPI { suspend fun getRoomSummary(@Path("roomIdOrAlias") roomidOrAlias: String, @Query("via") viaServers: List?): 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 } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index dbd0ae6f06..7939c74dce 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -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.FindReactionEventForUndoTask 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.ReportContentTask import org.matrix.android.sdk.internal.session.room.state.DefaultSendStateTask @@ -256,4 +258,7 @@ internal abstract class RoomModule { @Binds abstract fun bindGetRoomSummaryTask(task: DefaultGetRoomSummaryTask): GetRoomSummaryTask + + @Binds + abstract fun bindFetchThreadTimelineTask(task: DefaultFetchThreadTimelineTask): FetchThreadTimelineTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index d459e79a4a..02af20de23 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -21,26 +21,48 @@ import com.zhuinden.monarchy.Monarchy import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import io.realm.Realm 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.room.model.EventAnnotationsSummary 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.util.Cancelable import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional 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.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.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.di.MoshiProvider 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.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.configureWith +import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.fetchCopyMap import timber.log.Timber @@ -50,9 +72,12 @@ internal class DefaultRelationService @AssistedInject constructor( private val eventSenderProcessor: EventSenderProcessor, private val eventFactory: LocalEchoEventFactory, private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, + private val cryptoService: DefaultCryptoService, private val findReactionEventForUndoTask: FindReactionEventForUndoTask, private val fetchEditHistoryTask: FetchEditHistoryTask, + private val fetchThreadTimelineTask: FetchThreadTimelineTask, private val timelineEventMapper: TimelineEventMapper, + @UserId private val userId: String, @SessionDatabase private val monarchy: Monarchy, private val taskExecutor: TaskExecutor) : RelationService { @@ -192,7 +217,77 @@ internal class DefaultRelationService @AssistedInject constructor( 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 { + val results = fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(roomId, rootThreadEventId)) + var counter = 0 +// +// monarchy +// .awaitTransaction { realm -> +// val chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) +// +// val optimizedThreadSummaryMap = hashMapOf() +// 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 } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt new file mode 100644 index 0000000000..d62ce4158f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt @@ -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> { + 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 { + 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) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 69e56a85d0..a100c4635a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -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.createUIHandler import timber.log.Timber +import java.lang.Thread.sleep import java.util.Collections import java.util.UUID import java.util.concurrent.CopyOnWriteArrayList @@ -107,6 +108,7 @@ internal class DefaultTimeline( private val backwardsState = AtomicReference(TimelineState()) private val forwardsState = AtomicReference(TimelineState()) private var isFromThreadTimeline = false + private var rootThreadEventId: String? = null override val timelineID = UUID.randomUUID().toString() override val isLive @@ -151,9 +153,11 @@ internal class DefaultTimeline( override fun start(rootThreadEventId: String?) { if (isStarted.compareAndSet(false, true)) { isFromThreadTimeline = rootThreadEventId != null + this@DefaultTimeline.rootThreadEventId = rootThreadEventId Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId") timelineInput.listeners.add(this) BACKGROUND_HANDLER.post { + eventDecryptor.start() val realm = Realm.getInstance(realmConfiguration) backgroundRealm.set(realm) @@ -170,9 +174,10 @@ internal class DefaultTimeline( } timelineEvents = rootThreadEventId?.let { - TimelineEventEntity + val threadTimelineEvents = 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() @@ -180,7 +185,15 @@ 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}") timelineEvents.addChangeListener(eventsChangeListener) handleInitialLoad() @@ -330,17 +343,19 @@ 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)}") - updateState(Timeline.Direction.FORWARDS) { - it.copy( + updateState(Timeline.Direction.FORWARDS) { state -> + state.copy( 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( + updateState(Timeline.Direction.BACKWARDS) { state -> + state.copy( 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 { EventMapper.map(it) } - threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList) + threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList) } private fun buildTimelineEvent(eventEntity: TimelineEventEntity): TimelineEvent { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 62bcccb67a..552a7e63f6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -180,6 +180,15 @@ class RoomDetailViewModel @AssistedInject constructor( if (OutboundSessionKeySharingStrategy.WhenEnteringRoom == BuildConfig.outboundSessionKeySharingStrategy && room.isEncrypted()) { prepareForEncryption() } + + // Threads + initThreads() + } + + /** + * Threads specific initialization + */ + private fun initThreads() { markThreadTimelineAsReadLocal() 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 */ @@ -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 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) { viewModelScope.launch { // tryEmit doesn't work with SharedFlow without cache @@ -1125,6 +1145,8 @@ class RoomDetailViewModel @AssistedInject constructor( chatEffectManager.delegate = null chatEffectManager.dispose() callManager.removeProtocolsCheckerListener(this) + // we should also mark it as read here, for the scenario that the user + // is already in the thread timeline markThreadTimelineAsReadLocal() super.onCleared() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 20a3f34338..b29cf141d4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -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. val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId 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) { modelCache[prevDisplayableEventIndex] = null @@ -377,7 +382,11 @@ 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, 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 if (modelCache[position] == null || modelCache[position]?.isCacheable == false) { @@ -459,7 +468,12 @@ 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, partialState.isFromThreadTimeline())) { + if (!timelineEventVisibilityHelper.shouldShowEvent( + timelineEvent = event, + highlightedEventId = partialState.highlightedEventId, + isFromThreadTimeline = partialState.isFromThreadTimeline(), + rootThreadEventId = partialState.rootThreadEventId + )) { continue } // 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 { 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 } if (lastShownEventId == null) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 1c25f923cf..874d8f0b1e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -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, partialState.isFromThreadTimeline()) + val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2, eventIdToHighlight,partialState.rootThreadEventId, partialState.isFromThreadTimeline()) return if (mergedEvents.isEmpty()) { null } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 2f7c7fdc0f..1e915d2b29 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -42,8 +42,17 @@ 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, params.isFromThreadTimeline())) { - return buildEmptyItem(event, params.prevEvent, params.highlightedEventId, params.isFromThreadTimeline()) + if (!timelineEventVisibilityHelper.shouldShowEvent( + 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()) { // Message itemsX @@ -112,11 +121,24 @@ 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, 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 { - val isNotBlank = prevEvent == null || timelineEventVisibilityHelper.shouldShowEvent(prevEvent, highlightedEventId, isFromThreadTimeline) + private fun buildEmptyItem(timelineEvent: TimelineEvent, + 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_() .id(timelineEvent.localId) .eventId(timelineEvent.eventId) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt index 7efefc5209..e91f28cea6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt @@ -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. */ - private fun nextSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?, isFromThreadTimeline: Boolean): List { + private fun nextSameTypeEvents( + timelineEvents: List, + index: Int, + minSize: Int, + eventIdToHighlight: String?, + rootThreadEventId: String?, + isFromThreadTimeline: Boolean): List { if (index >= timelineEvents.size - 1) { return emptyList() } @@ -62,11 +68,18 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen } else { 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) { 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. */ - fun prevSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?, isFromThreadTimeline: Boolean): List { + fun prevSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?, rootThreadEventId: String?, isFromThreadTimeline: Boolean): List { val prevSub = timelineEvents.subList(0, index + 1) return prevSub .reversed() .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 * @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 (userPreferencesProvider.shouldShowHiddenEvents()) { 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. - return !timelineEvent.shouldBeHidden(isFromThreadTimeline) + return !timelineEvent.shouldBeHidden(rootThreadEventId, isFromThreadTimeline) } private fun TimelineEvent.isDisplayable(): Boolean { 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()) { return true } @@ -128,10 +146,18 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen return true } - if (BuildConfig.THREADING_ENABLED && !isFromThreadTimeline && root.isThread() && root.getRootThreadEventId() != null) { + if (BuildConfig.THREADING_ENABLED && !isFromThreadTimeline && root.isThread()) { return true } + if (BuildConfig.THREADING_ENABLED && isFromThreadTimeline) { + + //// + return if (root.getRootThreadEventId() == rootThreadEventId) { + false + } else root.eventId != rootThreadEventId + } + return false }