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 }