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,
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<Event>
}

View file

@ -82,7 +82,7 @@ internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity,
internal fun ChunkEntity.addTimelineEvent(roomId: String,
eventEntity: EventEntity,
direction: PaginationDirection,
roomMemberContentsByUser: Map<String, RoomMemberContent?>) {
roomMemberContentsByUser: Map<String, RoomMemberContent?>? = 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) {

View file

@ -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<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.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
}

View file

@ -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<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.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 {

View file

@ -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<TimelineEvent>) {
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()
}

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.
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) {

View file

@ -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 {

View file

@ -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)

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.
*/
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) {
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<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)
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
}