mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-29 06:28:45 +03:00
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:
parent
dcabaa0dab
commit
f06397023a
12 changed files with 313 additions and 52 deletions
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue