mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 05:31:21 +03:00
Timeline : remove filtering from SDK
This commit is contained in:
parent
3e8370cdc7
commit
d6d4293ea8
12 changed files with 51 additions and 441 deletions
|
@ -95,12 +95,6 @@ interface Timeline {
|
|||
*/
|
||||
fun getTimelineEventWithId(eventId: String?): TimelineEvent?
|
||||
|
||||
/**
|
||||
* Returns the first displayable events starting from eventId.
|
||||
* It does depend on the provided [TimelineSettings].
|
||||
*/
|
||||
fun getFirstDisplayableEventId(eventId: String): String?
|
||||
|
||||
interface Listener {
|
||||
/**
|
||||
* Call when the timeline has been updated through pagination or sync.
|
||||
|
|
|
@ -24,10 +24,6 @@ data class TimelineSettings(
|
|||
* The initial number of events to retrieve from cache. You might get less events if you don't have loaded enough yet.
|
||||
*/
|
||||
val initialSize: Int,
|
||||
/**
|
||||
* Filters for timeline event
|
||||
*/
|
||||
val filters: TimelineEventFilters = TimelineEventFilters(),
|
||||
/**
|
||||
* If true, will build read receipts for each event.
|
||||
*/
|
||||
|
|
|
@ -25,9 +25,9 @@ import javax.inject.Inject
|
|||
|
||||
internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) {
|
||||
|
||||
fun map(timelineEventEntity: TimelineEventEntity, buildReadReceipts: Boolean = true, correctedReadReceipts: List<ReadReceipt>? = null): TimelineEvent {
|
||||
fun map(timelineEventEntity: TimelineEventEntity, buildReadReceipts: Boolean = true): TimelineEvent {
|
||||
val readReceipts = if (buildReadReceipts) {
|
||||
correctedReadReceipts ?: timelineEventEntity.readReceipts
|
||||
timelineEventEntity.readReceipts
|
||||
?.let {
|
||||
readReceiptsSummaryMapper.map(it)
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ import org.matrix.android.sdk.api.NoOpMatrixCallback
|
|||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
|
@ -70,14 +69,12 @@ internal class DefaultTimeline(
|
|||
private val paginationTask: PaginationTask,
|
||||
private val timelineEventMapper: TimelineEventMapper,
|
||||
private val settings: TimelineSettings,
|
||||
private val hiddenReadReceipts: TimelineHiddenReadReceipts,
|
||||
private val timelineInput: TimelineInput,
|
||||
private val eventDecryptor: TimelineEventDecryptor,
|
||||
private val realmSessionProvider: RealmSessionProvider,
|
||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
private val readReceiptHandler: ReadReceiptHandler
|
||||
) : Timeline,
|
||||
TimelineHiddenReadReceipts.Delegate,
|
||||
TimelineInput.Listener,
|
||||
UIEchoManager.Listener {
|
||||
|
||||
|
@ -93,8 +90,7 @@ internal class DefaultTimeline(
|
|||
private val cancelableBag = CancelableBag()
|
||||
private val debouncer = Debouncer(mainHandler)
|
||||
|
||||
private lateinit var nonFilteredEvents: RealmResults<TimelineEventEntity>
|
||||
private lateinit var filteredEvents: RealmResults<TimelineEventEntity>
|
||||
private lateinit var timelineEvents: RealmResults<TimelineEventEntity>
|
||||
private lateinit var sendingEvents: RealmResults<TimelineEventEntity>
|
||||
|
||||
private var prevDisplayIndex: Int? = null
|
||||
|
@ -168,16 +164,9 @@ internal class DefaultTimeline(
|
|||
postSnapshot()
|
||||
}
|
||||
|
||||
nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
|
||||
filteredEvents = nonFilteredEvents.where()
|
||||
.filterEventsWithSettings(settings)
|
||||
.findAll()
|
||||
nonFilteredEvents.addChangeListener(eventsChangeListener)
|
||||
timelineEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
|
||||
timelineEvents.addChangeListener(eventsChangeListener)
|
||||
handleInitialLoad()
|
||||
if (settings.shouldHandleHiddenReadReceipts()) {
|
||||
hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this)
|
||||
}
|
||||
|
||||
loadRoomMembersTask
|
||||
.configureWith(LoadRoomMembersTask.Params(roomId)) {
|
||||
this.callback = NoOpMatrixCallback()
|
||||
|
@ -205,10 +194,6 @@ internal class DefaultTimeline(
|
|||
}
|
||||
}
|
||||
|
||||
private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean {
|
||||
return buildReadReceipts && (filters.filterEdits || filters.filterTypes)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
if (isStarted.compareAndSet(true, false)) {
|
||||
isReady.set(false)
|
||||
|
@ -220,11 +205,8 @@ internal class DefaultTimeline(
|
|||
if (this::sendingEvents.isInitialized) {
|
||||
sendingEvents.removeAllChangeListeners()
|
||||
}
|
||||
if (this::nonFilteredEvents.isInitialized) {
|
||||
nonFilteredEvents.removeAllChangeListeners()
|
||||
}
|
||||
if (settings.shouldHandleHiddenReadReceipts()) {
|
||||
hiddenReadReceipts.dispose()
|
||||
if (this::timelineEvents.isInitialized) {
|
||||
timelineEvents.removeAllChangeListeners()
|
||||
}
|
||||
clearAllValues()
|
||||
backgroundRealm.getAndSet(null).also {
|
||||
|
@ -256,48 +238,6 @@ internal class DefaultTimeline(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getFirstDisplayableEventId(eventId: String): String? {
|
||||
// If the item is built, the id is obviously displayable
|
||||
val builtIndex = builtEventsIdMap[eventId]
|
||||
if (builtIndex != null) {
|
||||
return eventId
|
||||
}
|
||||
// Otherwise, we should check if the event is in the db, but is hidden because of filters
|
||||
return realmSessionProvider.withRealm { localRealm ->
|
||||
val nonFilteredEvents = buildEventQuery(localRealm)
|
||||
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||
.findAll()
|
||||
|
||||
val nonFilteredEvent = nonFilteredEvents.where()
|
||||
.equalTo(TimelineEventEntityFields.EVENT_ID, eventId)
|
||||
.findFirst()
|
||||
|
||||
val filteredEvents = nonFilteredEvents.where()
|
||||
.filterEventsWithSettings(settings)
|
||||
.findAll()
|
||||
val isEventInDb = nonFilteredEvent != null
|
||||
|
||||
val isHidden = isEventInDb && filteredEvents.where()
|
||||
.equalTo(TimelineEventEntityFields.EVENT_ID, eventId)
|
||||
.findFirst() == null
|
||||
|
||||
if (isHidden) {
|
||||
val displayIndex = nonFilteredEvent?.displayIndex
|
||||
if (displayIndex != null) {
|
||||
// Then we are looking for the first displayable event after the hidden one
|
||||
val firstDisplayedEvent = filteredEvents.where()
|
||||
.lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, displayIndex)
|
||||
.findFirst()
|
||||
firstDisplayedEvent?.eventId
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasMoreToLoad(direction: Timeline.Direction): Boolean {
|
||||
return hasMoreInCache(direction) || !hasReachedEnd(direction)
|
||||
}
|
||||
|
@ -319,18 +259,6 @@ internal class DefaultTimeline(
|
|||
listeners.clear()
|
||||
}
|
||||
|
||||
// TimelineHiddenReadReceipts.Delegate
|
||||
|
||||
override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean {
|
||||
return rebuildEvent(eventId) { te ->
|
||||
te.copy(readReceipts = readReceipts)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReadReceiptsUpdated() {
|
||||
postSnapshot()
|
||||
}
|
||||
|
||||
override fun onNewTimelineEvents(roomId: String, eventIds: List<String>) {
|
||||
if (isLive && this.roomId == roomId) {
|
||||
listeners.forEach {
|
||||
|
@ -341,18 +269,13 @@ internal class DefaultTimeline(
|
|||
|
||||
override fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) {
|
||||
if (roomId != this.roomId || !isLive) return
|
||||
|
||||
val postSnapShot = uiEchoManager.onLocalEchoCreated(timelineEvent)
|
||||
|
||||
if (listOf(timelineEvent).filterEventsWithSettings(settings).isNotEmpty()) {
|
||||
listeners.forEach {
|
||||
uiEchoManager.onLocalEchoCreated(timelineEvent)
|
||||
listeners.forEach {
|
||||
tryOrNull {
|
||||
it.onNewTimelineEvents(listOf(timelineEvent.eventId))
|
||||
}
|
||||
}
|
||||
|
||||
if (postSnapShot) {
|
||||
postSnapshot()
|
||||
}
|
||||
postSnapshot()
|
||||
}
|
||||
|
||||
override fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) {
|
||||
|
@ -439,23 +362,21 @@ internal class DefaultTimeline(
|
|||
val builtSendingEvents = mutableListOf<TimelineEvent>()
|
||||
if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) {
|
||||
uiEchoManager.getInMemorySendingEvents()
|
||||
.filterSendingEventsTo(builtSendingEvents)
|
||||
.updateWithUiEchoInto(builtSendingEvents)
|
||||
sendingEvents
|
||||
.filter { timelineEvent ->
|
||||
builtSendingEvents.none { it.eventId == timelineEvent.eventId }
|
||||
}
|
||||
.map { timelineEventMapper.map(it) }
|
||||
.filterSendingEventsTo(builtSendingEvents)
|
||||
.updateWithUiEchoInto(builtSendingEvents)
|
||||
}
|
||||
return builtSendingEvents
|
||||
}
|
||||
|
||||
private fun List<TimelineEvent>.filterSendingEventsTo(target: MutableList<TimelineEvent>) {
|
||||
private fun List<TimelineEvent>.updateWithUiEchoInto(target: MutableList<TimelineEvent>) {
|
||||
target.addAll(
|
||||
// Filter out sending event that are not displayable!
|
||||
filterEventsWithSettings(settings)
|
||||
// Get most up to date send state (in memory)
|
||||
.map { uiEchoManager.updateSentStateWithUiEcho(it) }
|
||||
// Get most up to date send state (in memory)
|
||||
map { uiEchoManager.updateSentStateWithUiEcho(it) }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -465,14 +386,14 @@ internal class DefaultTimeline(
|
|||
|
||||
private fun getState(direction: Timeline.Direction): TimelineState {
|
||||
return when (direction) {
|
||||
Timeline.Direction.FORWARDS -> forwardsState.get()
|
||||
Timeline.Direction.FORWARDS -> forwardsState.get()
|
||||
Timeline.Direction.BACKWARDS -> backwardsState.get()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateState(direction: Timeline.Direction, update: (TimelineState) -> TimelineState) {
|
||||
val stateReference = when (direction) {
|
||||
Timeline.Direction.FORWARDS -> forwardsState
|
||||
Timeline.Direction.FORWARDS -> forwardsState
|
||||
Timeline.Direction.BACKWARDS -> backwardsState
|
||||
}
|
||||
val currentValue = stateReference.get()
|
||||
|
@ -487,9 +408,9 @@ internal class DefaultTimeline(
|
|||
var shouldFetchInitialEvent = false
|
||||
val currentInitialEventId = initialEventId
|
||||
val initialDisplayIndex = if (currentInitialEventId == null) {
|
||||
nonFilteredEvents.firstOrNull()?.displayIndex
|
||||
timelineEvents.firstOrNull()?.displayIndex
|
||||
} else {
|
||||
val initialEvent = nonFilteredEvents.where()
|
||||
val initialEvent = timelineEvents.where()
|
||||
.equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId)
|
||||
.findFirst()
|
||||
|
||||
|
@ -501,7 +422,7 @@ internal class DefaultTimeline(
|
|||
if (currentInitialEventId != null && shouldFetchInitialEvent) {
|
||||
fetchEvent(currentInitialEventId)
|
||||
} else {
|
||||
val count = filteredEvents.size.coerceAtMost(settings.initialSize)
|
||||
val count = timelineEvents.size.coerceAtMost(settings.initialSize)
|
||||
if (initialEventId == null) {
|
||||
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count)
|
||||
} else {
|
||||
|
@ -541,8 +462,7 @@ internal class DefaultTimeline(
|
|||
val eventEntity = results[index]
|
||||
eventEntity?.eventId?.let { eventId ->
|
||||
postSnapshot = rebuildEvent(eventId) {
|
||||
val builtEvent = buildTimelineEvent(eventEntity)
|
||||
listOf(builtEvent).filterEventsWithSettings(settings).firstOrNull()
|
||||
buildTimelineEvent(eventEntity)
|
||||
} || postSnapshot
|
||||
}
|
||||
}
|
||||
|
@ -563,9 +483,9 @@ internal class DefaultTimeline(
|
|||
// We are in the case where event exists, but we do not know the token.
|
||||
// Fetch (again) the last event to get a token
|
||||
val lastKnownEventId = if (direction == Timeline.Direction.FORWARDS) {
|
||||
nonFilteredEvents.firstOrNull()?.eventId
|
||||
timelineEvents.firstOrNull()?.eventId
|
||||
} else {
|
||||
nonFilteredEvents.lastOrNull()?.eventId
|
||||
timelineEvents.lastOrNull()?.eventId
|
||||
}
|
||||
if (lastKnownEventId == null) {
|
||||
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
|
||||
|
@ -636,7 +556,7 @@ internal class DefaultTimeline(
|
|||
* Return the current Chunk
|
||||
*/
|
||||
private fun getLiveChunk(): ChunkEntity? {
|
||||
return nonFilteredEvents.firstOrNull()?.chunk?.firstOrNull()
|
||||
return timelineEvents.firstOrNull()?.chunk?.firstOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -680,14 +600,13 @@ internal class DefaultTimeline(
|
|||
val time = System.currentTimeMillis() - start
|
||||
Timber.v("Built ${offsetResults.size} items from db in $time ms")
|
||||
// For the case where wo reach the lastForward chunk
|
||||
updateLoadingStates(filteredEvents)
|
||||
updateLoadingStates(timelineEvents)
|
||||
return offsetResults.size
|
||||
}
|
||||
|
||||
private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map(
|
||||
timelineEventEntity = eventEntity,
|
||||
buildReadReceipts = settings.buildReadReceipts,
|
||||
correctedReadReceipts = hiddenReadReceipts.correctedReadReceipts(eventEntity.eventId)
|
||||
buildReadReceipts = settings.buildReadReceipts
|
||||
).let {
|
||||
// eventually enhance with ui echo?
|
||||
(uiEchoManager.decorateEventWithReactionUiEcho(it) ?: it)
|
||||
|
@ -699,7 +618,7 @@ internal class DefaultTimeline(
|
|||
private fun getOffsetResults(startDisplayIndex: Int,
|
||||
direction: Timeline.Direction,
|
||||
count: Long): RealmResults<TimelineEventEntity> {
|
||||
val offsetQuery = filteredEvents.where()
|
||||
val offsetQuery = timelineEvents.where()
|
||||
if (direction == Timeline.Direction.BACKWARDS) {
|
||||
offsetQuery
|
||||
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||
|
@ -747,7 +666,7 @@ internal class DefaultTimeline(
|
|||
if (isReady.get().not()) {
|
||||
return@post
|
||||
}
|
||||
updateLoadingStates(filteredEvents)
|
||||
updateLoadingStates(timelineEvents)
|
||||
val snapshot = createSnapshot()
|
||||
val runnable = Runnable {
|
||||
listeners.forEach {
|
||||
|
@ -783,10 +702,10 @@ internal class DefaultTimeline(
|
|||
return object : MatrixCallback<TokenChunkEventPersistor.Result> {
|
||||
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
|
||||
when (data) {
|
||||
TokenChunkEventPersistor.Result.SUCCESS -> {
|
||||
TokenChunkEventPersistor.Result.SUCCESS -> {
|
||||
Timber.v("Success fetching $limit items $direction from pagination request")
|
||||
}
|
||||
TokenChunkEventPersistor.Result.REACHED_END -> {
|
||||
TokenChunkEventPersistor.Result.REACHED_END -> {
|
||||
postSnapshot()
|
||||
}
|
||||
TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->
|
||||
|
|
|
@ -52,7 +52,6 @@ internal class DefaultTimelineService @AssistedInject constructor(
|
|||
private val paginationTask: PaginationTask,
|
||||
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
|
||||
private val timelineEventMapper: TimelineEventMapper,
|
||||
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
|
||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
private val readReceiptHandler: ReadReceiptHandler
|
||||
) : TimelineService {
|
||||
|
@ -72,7 +71,6 @@ internal class DefaultTimelineService @AssistedInject constructor(
|
|||
paginationTask = paginationTask,
|
||||
timelineEventMapper = timelineEventMapper,
|
||||
settings = settings,
|
||||
hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
|
||||
timelineInput = timelineInput,
|
||||
eventDecryptor = eventDecryptor,
|
||||
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 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.timeline
|
||||
|
||||
import io.realm.RealmQuery
|
||||
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.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||
import org.matrix.android.sdk.internal.database.query.filterEvents
|
||||
|
||||
internal fun RealmQuery<TimelineEventEntity>.filterEventsWithSettings(settings: TimelineSettings): RealmQuery<TimelineEventEntity> {
|
||||
return filterEvents(settings.filters)
|
||||
}
|
||||
|
||||
internal fun List<TimelineEvent>.filterEventsWithSettings(settings: TimelineSettings): List<TimelineEvent> {
|
||||
return filter { event ->
|
||||
val filterType = !settings.filters.filterTypes
|
||||
|| settings.filters.allowedTypes.any { it.eventType == event.root.type && (it.stateKey == null || it.stateKey == event.root.senderId) }
|
||||
if (!filterType) return@filter false
|
||||
|
||||
val filterEdits = if (settings.filters.filterEdits && event.root.getClearType() == EventType.MESSAGE) {
|
||||
val messageContent = event.root.getClearContent().toModel<MessageContent>()
|
||||
messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE
|
||||
} else {
|
||||
true
|
||||
}
|
||||
if (!filterEdits) return@filter false
|
||||
|
||||
val filterRedacted = settings.filters.filterRedacted && event.root.isRedacted()
|
||||
!filterRedacted
|
||||
}
|
||||
}
|
|
@ -1,195 +0,0 @@
|
|||
/*
|
||||
* Copyright 2020 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.timeline
|
||||
|
||||
import android.util.SparseArray
|
||||
import io.realm.OrderedRealmCollectionChangeListener
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.RealmResults
|
||||
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper
|
||||
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntityFields
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
|
||||
import org.matrix.android.sdk.internal.database.query.TimelineEventFilter
|
||||
import org.matrix.android.sdk.internal.database.query.whereInRoom
|
||||
|
||||
/**
|
||||
* This class is responsible for handling the read receipts for hidden events (check [TimelineSettings] to see filtering).
|
||||
* When an hidden event has read receipts, we want to transfer these read receipts on the first older displayed event.
|
||||
* It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription.
|
||||
*/
|
||||
internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
|
||||
private val roomId: String,
|
||||
private val settings: TimelineSettings) {
|
||||
|
||||
interface Delegate {
|
||||
fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean
|
||||
fun onReadReceiptsUpdated()
|
||||
}
|
||||
|
||||
private val correctedReadReceiptsEventByIndex = SparseArray<String>()
|
||||
private val correctedReadReceiptsByEvent = HashMap<String, MutableList<ReadReceipt>>()
|
||||
|
||||
private lateinit var hiddenReadReceipts: RealmResults<ReadReceiptsSummaryEntity>
|
||||
private lateinit var nonFilteredEvents: RealmResults<TimelineEventEntity>
|
||||
private lateinit var filteredEvents: RealmResults<TimelineEventEntity>
|
||||
private lateinit var delegate: Delegate
|
||||
|
||||
private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener<RealmResults<ReadReceiptsSummaryEntity>> { collection, changeSet ->
|
||||
if (!collection.isLoaded || !collection.isValid) {
|
||||
return@OrderedRealmCollectionChangeListener
|
||||
}
|
||||
var hasChange = false
|
||||
// Deletion here means we don't have any readReceipts for the given hidden events
|
||||
changeSet.deletions.forEach {
|
||||
val eventId = correctedReadReceiptsEventByIndex.get(it, "")
|
||||
val timelineEvent = filteredEvents.where()
|
||||
.equalTo(TimelineEventEntityFields.EVENT_ID, eventId)
|
||||
.findFirst()
|
||||
|
||||
// We are rebuilding the corresponding event with only his own RR
|
||||
val readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts)
|
||||
hasChange = delegate.rebuildEvent(eventId, readReceipts) || hasChange
|
||||
}
|
||||
correctedReadReceiptsEventByIndex.clear()
|
||||
correctedReadReceiptsByEvent.clear()
|
||||
for (index in 0 until hiddenReadReceipts.size) {
|
||||
val summary = hiddenReadReceipts[index] ?: continue
|
||||
val timelineEvent = summary.timelineEvent?.firstOrNull() ?: continue
|
||||
val isLoaded = nonFilteredEvents.where()
|
||||
.equalTo(TimelineEventEntityFields.EVENT_ID, timelineEvent.eventId).findFirst() != null
|
||||
val displayIndex = timelineEvent.displayIndex
|
||||
|
||||
if (isLoaded) {
|
||||
// Then we are looking for the first displayable event after the hidden one
|
||||
val firstDisplayedEvent = filteredEvents.where()
|
||||
.lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, displayIndex)
|
||||
.findFirst()
|
||||
|
||||
// If we find one, we should
|
||||
if (firstDisplayedEvent != null) {
|
||||
correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId)
|
||||
correctedReadReceiptsByEvent
|
||||
.getOrPut(firstDisplayedEvent.eventId, {
|
||||
ArrayList(readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts))
|
||||
})
|
||||
.addAll(readReceiptsSummaryMapper.map(summary))
|
||||
}
|
||||
}
|
||||
}
|
||||
if (correctedReadReceiptsByEvent.isNotEmpty()) {
|
||||
correctedReadReceiptsByEvent.forEach { (eventId, correctedReadReceipts) ->
|
||||
val sortedReadReceipts = correctedReadReceipts.sortedByDescending {
|
||||
it.originServerTs
|
||||
}
|
||||
hasChange = delegate.rebuildEvent(eventId, sortedReadReceipts) || hasChange
|
||||
}
|
||||
}
|
||||
if (hasChange) {
|
||||
delegate.onReadReceiptsUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the realm query subscription. Has to be called on an HandlerThread
|
||||
*/
|
||||
fun start(realm: Realm,
|
||||
filteredEvents: RealmResults<TimelineEventEntity>,
|
||||
nonFilteredEvents: RealmResults<TimelineEventEntity>,
|
||||
delegate: Delegate) {
|
||||
this.filteredEvents = filteredEvents
|
||||
this.nonFilteredEvents = nonFilteredEvents
|
||||
this.delegate = delegate
|
||||
// We are looking for read receipts set on hidden events.
|
||||
// We only accept those with a timelineEvent (so coming from pagination/sync).
|
||||
this.hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId)
|
||||
.isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.`$`)
|
||||
.isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`)
|
||||
.filterReceiptsWithSettings()
|
||||
.findAllAsync()
|
||||
.also { it.addChangeListener(hiddenReadReceiptsListener) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose the realm query subscription. Has to be called on an HandlerThread
|
||||
*/
|
||||
fun dispose() {
|
||||
if (this::hiddenReadReceipts.isInitialized) {
|
||||
this.hiddenReadReceipts.removeAllChangeListeners()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current corrected [ReadReceipt] list for an event, or null
|
||||
*/
|
||||
fun correctedReadReceipts(eventId: String?): List<ReadReceipt>? {
|
||||
return correctedReadReceiptsByEvent[eventId]
|
||||
}
|
||||
|
||||
/**
|
||||
* We are looking for receipts related to filtered events. So, it's the opposite of [DefaultTimeline.filterEventsWithSettings] method.
|
||||
*/
|
||||
private fun RealmQuery<ReadReceiptsSummaryEntity>.filterReceiptsWithSettings(): RealmQuery<ReadReceiptsSummaryEntity> {
|
||||
beginGroup()
|
||||
var needOr = false
|
||||
if (settings.filters.filterTypes) {
|
||||
beginGroup()
|
||||
// Events: A, B, C, D, (E and S1), F, G, (H and S1), I
|
||||
// Allowed: A, B, C, (E and S1), G, (H and S2)
|
||||
// Result: D, F, H, I
|
||||
settings.filters.allowedTypes.forEachIndexed { index, filter ->
|
||||
if (filter.stateKey == null) {
|
||||
notEqualTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.TYPE}", filter.eventType)
|
||||
} else {
|
||||
beginGroup()
|
||||
notEqualTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.TYPE}", filter.eventType)
|
||||
or()
|
||||
notEqualTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.STATE_KEY}", filter.stateKey)
|
||||
endGroup()
|
||||
}
|
||||
if (index != settings.filters.allowedTypes.size - 1) {
|
||||
and()
|
||||
}
|
||||
}
|
||||
endGroup()
|
||||
needOr = true
|
||||
}
|
||||
if (settings.filters.filterUseless) {
|
||||
if (needOr) or()
|
||||
equalTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.IS_USELESS}", true)
|
||||
needOr = true
|
||||
}
|
||||
if (settings.filters.filterEdits) {
|
||||
if (needOr) or()
|
||||
like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.CONTENT}", TimelineEventFilter.Content.EDIT)
|
||||
or()
|
||||
like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.CONTENT}", TimelineEventFilter.Content.RESPONSE)
|
||||
needOr = true
|
||||
}
|
||||
if (settings.filters.filterRedacted) {
|
||||
if (needOr) or()
|
||||
like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.UNSIGNED_DATA}", TimelineEventFilter.Unsigned.REDACTED)
|
||||
}
|
||||
endGroup()
|
||||
return this
|
||||
}
|
||||
}
|
|
@ -70,15 +70,13 @@ internal class UIEchoManager(
|
|||
return existingState != sendState
|
||||
}
|
||||
|
||||
// return true if should update
|
||||
fun onLocalEchoCreated(timelineEvent: TimelineEvent): Boolean {
|
||||
var postSnapshot = false
|
||||
fun onLocalEchoCreated(timelineEvent: TimelineEvent) {
|
||||
|
||||
// Manage some ui echos (do it before filter because actual event could be filtered out)
|
||||
when (timelineEvent.root.getClearType()) {
|
||||
EventType.REDACTION -> {
|
||||
}
|
||||
EventType.REACTION -> {
|
||||
EventType.REACTION -> {
|
||||
val content = timelineEvent.root.content?.toModel<ReactionContent>()
|
||||
if (RelationType.ANNOTATION == content?.relatesTo?.type) {
|
||||
val reaction = content.relatesTo.key
|
||||
|
@ -91,21 +89,14 @@ internal class UIEchoManager(
|
|||
reaction = reaction
|
||||
)
|
||||
)
|
||||
postSnapshot = listener.rebuildEvent(relatedEventID) {
|
||||
listener.rebuildEvent(relatedEventID) {
|
||||
decorateEventWithReactionUiEcho(it)
|
||||
} || postSnapshot
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// do not add events that would have been filtered
|
||||
if (listOf(timelineEvent).filterEventsWithSettings(settings).isNotEmpty()) {
|
||||
Timber.v("On local echo created: ${timelineEvent.eventId}")
|
||||
inMemorySendingEvents.add(0, timelineEvent)
|
||||
postSnapshot = true
|
||||
}
|
||||
|
||||
return postSnapshot
|
||||
Timber.v("On local echo created: ${timelineEvent.eventId}")
|
||||
inMemorySendingEvents.add(0, timelineEvent)
|
||||
}
|
||||
|
||||
fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent? {
|
||||
|
|
|
@ -1205,7 +1205,6 @@ class RoomDetailFragment @Inject constructor(
|
|||
if (summary?.membership == Membership.JOIN) {
|
||||
views.jumpToBottomView.count = summary.notificationCount
|
||||
views.jumpToBottomView.drawBadge = summary.hasUnreadMessages
|
||||
scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
|
||||
timelineEventController.update(state)
|
||||
views.inviteView.visibility = View.GONE
|
||||
if (state.tombstoneEvent == null) {
|
||||
|
|
|
@ -1156,16 +1156,15 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) {
|
||||
stopTrackingUnreadMessages()
|
||||
val targetEventId: String = action.eventId
|
||||
val correctedEventId = timeline.getFirstDisplayableEventId(targetEventId) ?: targetEventId
|
||||
val indexOfEvent = timeline.getIndexOfEvent(correctedEventId)
|
||||
val indexOfEvent = timeline.getIndexOfEvent(targetEventId)
|
||||
if (indexOfEvent == null) {
|
||||
// Event is not already in RAM
|
||||
timeline.restartWithEventId(targetEventId)
|
||||
}
|
||||
if (action.highlight) {
|
||||
setState { copy(highlightedEventId = correctedEventId) }
|
||||
setState { copy(highlightedEventId = targetEventId) }
|
||||
}
|
||||
_viewEvents.post(RoomDetailViewEvents.NavigateToEvent(correctedEventId))
|
||||
_viewEvents.post(RoomDetailViewEvents.NavigateToEvent(targetEventId))
|
||||
}
|
||||
|
||||
private fun handleResendEvent(action: RoomDetailAction.ResendMessage) {
|
||||
|
@ -1389,15 +1388,12 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
private fun computeUnreadState(events: List<TimelineEvent>, roomSummary: RoomSummary): UnreadState {
|
||||
if (events.isEmpty()) return UnreadState.Unknown
|
||||
val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown
|
||||
val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot)
|
||||
val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId)
|
||||
if (firstDisplayableEventId == null || firstDisplayableEventIndex == null) {
|
||||
return if (timeline.isLive) {
|
||||
UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
|
||||
} else {
|
||||
UnreadState.Unknown
|
||||
}
|
||||
}
|
||||
val firstDisplayableEventIndex = timeline.getIndexOfEvent(readMarkerIdSnapshot)
|
||||
?: return if (timeline.isLive) {
|
||||
UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
|
||||
} else {
|
||||
UnreadState.Unknown
|
||||
}
|
||||
for (i in (firstDisplayableEventIndex - 1) downTo 0) {
|
||||
val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown
|
||||
val eventId = timelineEvent.root.eventId ?: return UnreadState.Unknown
|
||||
|
|
|
@ -33,8 +33,6 @@ class ScrollOnHighlightedEventCallback(private val recyclerView: RecyclerView,
|
|||
|
||||
private val scheduledEventId = AtomicReference<String?>()
|
||||
|
||||
var timeline: Timeline? = null
|
||||
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
scrollIfNeeded()
|
||||
}
|
||||
|
@ -45,9 +43,7 @@ class ScrollOnHighlightedEventCallback(private val recyclerView: RecyclerView,
|
|||
|
||||
private fun scrollIfNeeded() {
|
||||
val eventId = scheduledEventId.get() ?: return
|
||||
val nonNullTimeline = timeline ?: return
|
||||
val correctedEventId = nonNullTimeline.getFirstDisplayableEventId(eventId)
|
||||
val positionToScroll = timelineEventController.searchPositionOfEvent(correctedEventId)
|
||||
val positionToScroll = timelineEventController.searchPositionOfEvent(eventId)
|
||||
if (positionToScroll != null) {
|
||||
val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||
val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition()
|
||||
|
|
|
@ -17,48 +17,14 @@
|
|||
package im.vector.app.features.home.room.detail.timeline.helper
|
||||
|
||||
import im.vector.app.core.resources.UserPreferencesProvider
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.room.timeline.EventTypeFilter
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineSettingsFactory @Inject constructor(
|
||||
private val userPreferencesProvider: UserPreferencesProvider,
|
||||
private val session: Session
|
||||
) {
|
||||
class TimelineSettingsFactory @Inject constructor(private val userPreferencesProvider: UserPreferencesProvider) {
|
||||
|
||||
fun create(): TimelineSettings {
|
||||
return if (userPreferencesProvider.shouldShowHiddenEvents()) {
|
||||
TimelineSettings(
|
||||
initialSize = 30,
|
||||
filters = TimelineEventFilters(
|
||||
filterEdits = false,
|
||||
filterRedacted = userPreferencesProvider.shouldShowRedactedMessages().not(),
|
||||
filterUseless = false,
|
||||
filterTypes = false),
|
||||
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
|
||||
} else {
|
||||
val allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES.createAllowedEventTypeFilters()
|
||||
TimelineSettings(
|
||||
initialSize = 30,
|
||||
filters = TimelineEventFilters(
|
||||
filterEdits = true,
|
||||
filterRedacted = userPreferencesProvider.shouldShowRedactedMessages().not(),
|
||||
filterUseless = true,
|
||||
filterTypes = true,
|
||||
allowedTypes = allowedTypes),
|
||||
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<String>.createAllowedEventTypeFilters(): List<EventTypeFilter> {
|
||||
return map {
|
||||
EventTypeFilter(
|
||||
eventType = it,
|
||||
stateKey = if (it == EventType.STATE_ROOM_MEMBER && !userPreferencesProvider.shouldShowRoomMemberStateEvents()) session.myUserId else null
|
||||
)
|
||||
}
|
||||
return TimelineSettings(
|
||||
initialSize = 30,
|
||||
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue