Timeline : remove filtering from SDK

This commit is contained in:
ganfra 2021-03-29 16:50:12 +02:00
parent 3e8370cdc7
commit d6d4293ea8
12 changed files with 51 additions and 441 deletions

View file

@ -95,12 +95,6 @@ interface Timeline {
*/ */
fun getTimelineEventWithId(eventId: String?): TimelineEvent? 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 { interface Listener {
/** /**
* Call when the timeline has been updated through pagination or sync. * Call when the timeline has been updated through pagination or sync.

View file

@ -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. * 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, val initialSize: Int,
/**
* Filters for timeline event
*/
val filters: TimelineEventFilters = TimelineEventFilters(),
/** /**
* If true, will build read receipts for each event. * If true, will build read receipts for each event.
*/ */

View file

@ -25,9 +25,9 @@ import javax.inject.Inject
internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) { 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) { val readReceipts = if (buildReadReceipts) {
correctedReadReceipts ?: timelineEventEntity.readReceipts timelineEventEntity.readReceipts
?.let { ?.let {
readReceiptsSummaryMapper.map(it) readReceiptsSummaryMapper.map(it)
} }

View file

@ -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.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull 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.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.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@ -70,14 +69,12 @@ internal class DefaultTimeline(
private val paginationTask: PaginationTask, private val paginationTask: PaginationTask,
private val timelineEventMapper: TimelineEventMapper, private val timelineEventMapper: TimelineEventMapper,
private val settings: TimelineSettings, private val settings: TimelineSettings,
private val hiddenReadReceipts: TimelineHiddenReadReceipts,
private val timelineInput: TimelineInput, private val timelineInput: TimelineInput,
private val eventDecryptor: TimelineEventDecryptor, private val eventDecryptor: TimelineEventDecryptor,
private val realmSessionProvider: RealmSessionProvider, private val realmSessionProvider: RealmSessionProvider,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,
private val readReceiptHandler: ReadReceiptHandler private val readReceiptHandler: ReadReceiptHandler
) : Timeline, ) : Timeline,
TimelineHiddenReadReceipts.Delegate,
TimelineInput.Listener, TimelineInput.Listener,
UIEchoManager.Listener { UIEchoManager.Listener {
@ -93,8 +90,7 @@ internal class DefaultTimeline(
private val cancelableBag = CancelableBag() private val cancelableBag = CancelableBag()
private val debouncer = Debouncer(mainHandler) private val debouncer = Debouncer(mainHandler)
private lateinit var nonFilteredEvents: RealmResults<TimelineEventEntity> private lateinit var timelineEvents: RealmResults<TimelineEventEntity>
private lateinit var filteredEvents: RealmResults<TimelineEventEntity>
private lateinit var sendingEvents: RealmResults<TimelineEventEntity> private lateinit var sendingEvents: RealmResults<TimelineEventEntity>
private var prevDisplayIndex: Int? = null private var prevDisplayIndex: Int? = null
@ -168,16 +164,9 @@ internal class DefaultTimeline(
postSnapshot() postSnapshot()
} }
nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() timelineEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll()
filteredEvents = nonFilteredEvents.where() timelineEvents.addChangeListener(eventsChangeListener)
.filterEventsWithSettings(settings)
.findAll()
nonFilteredEvents.addChangeListener(eventsChangeListener)
handleInitialLoad() handleInitialLoad()
if (settings.shouldHandleHiddenReadReceipts()) {
hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this)
}
loadRoomMembersTask loadRoomMembersTask
.configureWith(LoadRoomMembersTask.Params(roomId)) { .configureWith(LoadRoomMembersTask.Params(roomId)) {
this.callback = NoOpMatrixCallback() this.callback = NoOpMatrixCallback()
@ -205,10 +194,6 @@ internal class DefaultTimeline(
} }
} }
private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean {
return buildReadReceipts && (filters.filterEdits || filters.filterTypes)
}
override fun dispose() { override fun dispose() {
if (isStarted.compareAndSet(true, false)) { if (isStarted.compareAndSet(true, false)) {
isReady.set(false) isReady.set(false)
@ -220,11 +205,8 @@ internal class DefaultTimeline(
if (this::sendingEvents.isInitialized) { if (this::sendingEvents.isInitialized) {
sendingEvents.removeAllChangeListeners() sendingEvents.removeAllChangeListeners()
} }
if (this::nonFilteredEvents.isInitialized) { if (this::timelineEvents.isInitialized) {
nonFilteredEvents.removeAllChangeListeners() timelineEvents.removeAllChangeListeners()
}
if (settings.shouldHandleHiddenReadReceipts()) {
hiddenReadReceipts.dispose()
} }
clearAllValues() clearAllValues()
backgroundRealm.getAndSet(null).also { 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 { override fun hasMoreToLoad(direction: Timeline.Direction): Boolean {
return hasMoreInCache(direction) || !hasReachedEnd(direction) return hasMoreInCache(direction) || !hasReachedEnd(direction)
} }
@ -319,18 +259,6 @@ internal class DefaultTimeline(
listeners.clear() 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>) { override fun onNewTimelineEvents(roomId: String, eventIds: List<String>) {
if (isLive && this.roomId == roomId) { if (isLive && this.roomId == roomId) {
listeners.forEach { listeners.forEach {
@ -341,18 +269,13 @@ internal class DefaultTimeline(
override fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) { override fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) {
if (roomId != this.roomId || !isLive) return if (roomId != this.roomId || !isLive) return
uiEchoManager.onLocalEchoCreated(timelineEvent)
val postSnapShot = uiEchoManager.onLocalEchoCreated(timelineEvent) listeners.forEach {
tryOrNull {
if (listOf(timelineEvent).filterEventsWithSettings(settings).isNotEmpty()) {
listeners.forEach {
it.onNewTimelineEvents(listOf(timelineEvent.eventId)) it.onNewTimelineEvents(listOf(timelineEvent.eventId))
} }
} }
postSnapshot()
if (postSnapShot) {
postSnapshot()
}
} }
override fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) { override fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) {
@ -439,23 +362,21 @@ internal class DefaultTimeline(
val builtSendingEvents = mutableListOf<TimelineEvent>() val builtSendingEvents = mutableListOf<TimelineEvent>()
if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) { if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) {
uiEchoManager.getInMemorySendingEvents() uiEchoManager.getInMemorySendingEvents()
.filterSendingEventsTo(builtSendingEvents) .updateWithUiEchoInto(builtSendingEvents)
sendingEvents sendingEvents
.filter { timelineEvent -> .filter { timelineEvent ->
builtSendingEvents.none { it.eventId == timelineEvent.eventId } builtSendingEvents.none { it.eventId == timelineEvent.eventId }
} }
.map { timelineEventMapper.map(it) } .map { timelineEventMapper.map(it) }
.filterSendingEventsTo(builtSendingEvents) .updateWithUiEchoInto(builtSendingEvents)
} }
return builtSendingEvents return builtSendingEvents
} }
private fun List<TimelineEvent>.filterSendingEventsTo(target: MutableList<TimelineEvent>) { private fun List<TimelineEvent>.updateWithUiEchoInto(target: MutableList<TimelineEvent>) {
target.addAll( target.addAll(
// Filter out sending event that are not displayable! // Get most up to date send state (in memory)
filterEventsWithSettings(settings) 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 { private fun getState(direction: Timeline.Direction): TimelineState {
return when (direction) { return when (direction) {
Timeline.Direction.FORWARDS -> forwardsState.get() Timeline.Direction.FORWARDS -> forwardsState.get()
Timeline.Direction.BACKWARDS -> backwardsState.get() Timeline.Direction.BACKWARDS -> backwardsState.get()
} }
} }
private fun updateState(direction: Timeline.Direction, update: (TimelineState) -> TimelineState) { private fun updateState(direction: Timeline.Direction, update: (TimelineState) -> TimelineState) {
val stateReference = when (direction) { val stateReference = when (direction) {
Timeline.Direction.FORWARDS -> forwardsState Timeline.Direction.FORWARDS -> forwardsState
Timeline.Direction.BACKWARDS -> backwardsState Timeline.Direction.BACKWARDS -> backwardsState
} }
val currentValue = stateReference.get() val currentValue = stateReference.get()
@ -487,9 +408,9 @@ internal class DefaultTimeline(
var shouldFetchInitialEvent = false var shouldFetchInitialEvent = false
val currentInitialEventId = initialEventId val currentInitialEventId = initialEventId
val initialDisplayIndex = if (currentInitialEventId == null) { val initialDisplayIndex = if (currentInitialEventId == null) {
nonFilteredEvents.firstOrNull()?.displayIndex timelineEvents.firstOrNull()?.displayIndex
} else { } else {
val initialEvent = nonFilteredEvents.where() val initialEvent = timelineEvents.where()
.equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId) .equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId)
.findFirst() .findFirst()
@ -501,7 +422,7 @@ internal class DefaultTimeline(
if (currentInitialEventId != null && shouldFetchInitialEvent) { if (currentInitialEventId != null && shouldFetchInitialEvent) {
fetchEvent(currentInitialEventId) fetchEvent(currentInitialEventId)
} else { } else {
val count = filteredEvents.size.coerceAtMost(settings.initialSize) val count = timelineEvents.size.coerceAtMost(settings.initialSize)
if (initialEventId == null) { if (initialEventId == null) {
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count)
} else { } else {
@ -541,8 +462,7 @@ internal class DefaultTimeline(
val eventEntity = results[index] val eventEntity = results[index]
eventEntity?.eventId?.let { eventId -> eventEntity?.eventId?.let { eventId ->
postSnapshot = rebuildEvent(eventId) { postSnapshot = rebuildEvent(eventId) {
val builtEvent = buildTimelineEvent(eventEntity) buildTimelineEvent(eventEntity)
listOf(builtEvent).filterEventsWithSettings(settings).firstOrNull()
} || postSnapshot } || postSnapshot
} }
} }
@ -563,9 +483,9 @@ internal class DefaultTimeline(
// We are in the case where event exists, but we do not know the token. // We are in the case where event exists, but we do not know the token.
// Fetch (again) the last event to get a token // Fetch (again) the last event to get a token
val lastKnownEventId = if (direction == Timeline.Direction.FORWARDS) { val lastKnownEventId = if (direction == Timeline.Direction.FORWARDS) {
nonFilteredEvents.firstOrNull()?.eventId timelineEvents.firstOrNull()?.eventId
} else { } else {
nonFilteredEvents.lastOrNull()?.eventId timelineEvents.lastOrNull()?.eventId
} }
if (lastKnownEventId == null) { if (lastKnownEventId == null) {
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
@ -636,7 +556,7 @@ internal class DefaultTimeline(
* Return the current Chunk * Return the current Chunk
*/ */
private fun getLiveChunk(): ChunkEntity? { 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 val time = System.currentTimeMillis() - start
Timber.v("Built ${offsetResults.size} items from db in $time ms") Timber.v("Built ${offsetResults.size} items from db in $time ms")
// For the case where wo reach the lastForward chunk // For the case where wo reach the lastForward chunk
updateLoadingStates(filteredEvents) updateLoadingStates(timelineEvents)
return offsetResults.size return offsetResults.size
} }
private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map( private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map(
timelineEventEntity = eventEntity, timelineEventEntity = eventEntity,
buildReadReceipts = settings.buildReadReceipts, buildReadReceipts = settings.buildReadReceipts
correctedReadReceipts = hiddenReadReceipts.correctedReadReceipts(eventEntity.eventId)
).let { ).let {
// eventually enhance with ui echo? // eventually enhance with ui echo?
(uiEchoManager.decorateEventWithReactionUiEcho(it) ?: it) (uiEchoManager.decorateEventWithReactionUiEcho(it) ?: it)
@ -699,7 +618,7 @@ internal class DefaultTimeline(
private fun getOffsetResults(startDisplayIndex: Int, private fun getOffsetResults(startDisplayIndex: Int,
direction: Timeline.Direction, direction: Timeline.Direction,
count: Long): RealmResults<TimelineEventEntity> { count: Long): RealmResults<TimelineEventEntity> {
val offsetQuery = filteredEvents.where() val offsetQuery = timelineEvents.where()
if (direction == Timeline.Direction.BACKWARDS) { if (direction == Timeline.Direction.BACKWARDS) {
offsetQuery offsetQuery
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
@ -747,7 +666,7 @@ internal class DefaultTimeline(
if (isReady.get().not()) { if (isReady.get().not()) {
return@post return@post
} }
updateLoadingStates(filteredEvents) updateLoadingStates(timelineEvents)
val snapshot = createSnapshot() val snapshot = createSnapshot()
val runnable = Runnable { val runnable = Runnable {
listeners.forEach { listeners.forEach {
@ -783,10 +702,10 @@ internal class DefaultTimeline(
return object : MatrixCallback<TokenChunkEventPersistor.Result> { return object : MatrixCallback<TokenChunkEventPersistor.Result> {
override fun onSuccess(data: TokenChunkEventPersistor.Result) { override fun onSuccess(data: TokenChunkEventPersistor.Result) {
when (data) { when (data) {
TokenChunkEventPersistor.Result.SUCCESS -> { TokenChunkEventPersistor.Result.SUCCESS -> {
Timber.v("Success fetching $limit items $direction from pagination request") Timber.v("Success fetching $limit items $direction from pagination request")
} }
TokenChunkEventPersistor.Result.REACHED_END -> { TokenChunkEventPersistor.Result.REACHED_END -> {
postSnapshot() postSnapshot()
} }
TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->

View file

@ -52,7 +52,6 @@ internal class DefaultTimelineService @AssistedInject constructor(
private val paginationTask: PaginationTask, private val paginationTask: PaginationTask,
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
private val timelineEventMapper: TimelineEventMapper, private val timelineEventMapper: TimelineEventMapper,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,
private val readReceiptHandler: ReadReceiptHandler private val readReceiptHandler: ReadReceiptHandler
) : TimelineService { ) : TimelineService {
@ -72,7 +71,6 @@ internal class DefaultTimelineService @AssistedInject constructor(
paginationTask = paginationTask, paginationTask = paginationTask,
timelineEventMapper = timelineEventMapper, timelineEventMapper = timelineEventMapper,
settings = settings, settings = settings,
hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
timelineInput = timelineInput, timelineInput = timelineInput,
eventDecryptor = eventDecryptor, eventDecryptor = eventDecryptor,
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,

View file

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

View file

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

View file

@ -70,15 +70,13 @@ internal class UIEchoManager(
return existingState != sendState return existingState != sendState
} }
// return true if should update fun onLocalEchoCreated(timelineEvent: TimelineEvent) {
fun onLocalEchoCreated(timelineEvent: TimelineEvent): Boolean {
var postSnapshot = false
// Manage some ui echos (do it before filter because actual event could be filtered out) // Manage some ui echos (do it before filter because actual event could be filtered out)
when (timelineEvent.root.getClearType()) { when (timelineEvent.root.getClearType()) {
EventType.REDACTION -> { EventType.REDACTION -> {
} }
EventType.REACTION -> { EventType.REACTION -> {
val content = timelineEvent.root.content?.toModel<ReactionContent>() val content = timelineEvent.root.content?.toModel<ReactionContent>()
if (RelationType.ANNOTATION == content?.relatesTo?.type) { if (RelationType.ANNOTATION == content?.relatesTo?.type) {
val reaction = content.relatesTo.key val reaction = content.relatesTo.key
@ -91,21 +89,14 @@ internal class UIEchoManager(
reaction = reaction reaction = reaction
) )
) )
postSnapshot = listener.rebuildEvent(relatedEventID) { listener.rebuildEvent(relatedEventID) {
decorateEventWithReactionUiEcho(it) decorateEventWithReactionUiEcho(it)
} || postSnapshot }
} }
} }
} }
Timber.v("On local echo created: ${timelineEvent.eventId}")
// do not add events that would have been filtered inMemorySendingEvents.add(0, timelineEvent)
if (listOf(timelineEvent).filterEventsWithSettings(settings).isNotEmpty()) {
Timber.v("On local echo created: ${timelineEvent.eventId}")
inMemorySendingEvents.add(0, timelineEvent)
postSnapshot = true
}
return postSnapshot
} }
fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent? { fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent? {

View file

@ -1205,7 +1205,6 @@ class RoomDetailFragment @Inject constructor(
if (summary?.membership == Membership.JOIN) { if (summary?.membership == Membership.JOIN) {
views.jumpToBottomView.count = summary.notificationCount views.jumpToBottomView.count = summary.notificationCount
views.jumpToBottomView.drawBadge = summary.hasUnreadMessages views.jumpToBottomView.drawBadge = summary.hasUnreadMessages
scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
timelineEventController.update(state) timelineEventController.update(state)
views.inviteView.visibility = View.GONE views.inviteView.visibility = View.GONE
if (state.tombstoneEvent == null) { if (state.tombstoneEvent == null) {

View file

@ -1156,16 +1156,15 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) { private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) {
stopTrackingUnreadMessages() stopTrackingUnreadMessages()
val targetEventId: String = action.eventId val targetEventId: String = action.eventId
val correctedEventId = timeline.getFirstDisplayableEventId(targetEventId) ?: targetEventId val indexOfEvent = timeline.getIndexOfEvent(targetEventId)
val indexOfEvent = timeline.getIndexOfEvent(correctedEventId)
if (indexOfEvent == null) { if (indexOfEvent == null) {
// Event is not already in RAM // Event is not already in RAM
timeline.restartWithEventId(targetEventId) timeline.restartWithEventId(targetEventId)
} }
if (action.highlight) { 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) { private fun handleResendEvent(action: RoomDetailAction.ResendMessage) {
@ -1389,15 +1388,12 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun computeUnreadState(events: List<TimelineEvent>, roomSummary: RoomSummary): UnreadState { private fun computeUnreadState(events: List<TimelineEvent>, roomSummary: RoomSummary): UnreadState {
if (events.isEmpty()) return UnreadState.Unknown if (events.isEmpty()) return UnreadState.Unknown
val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown
val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot) val firstDisplayableEventIndex = timeline.getIndexOfEvent(readMarkerIdSnapshot)
val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId) ?: return if (timeline.isLive) {
if (firstDisplayableEventId == null || firstDisplayableEventIndex == null) { UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
return if (timeline.isLive) { } else {
UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot) UnreadState.Unknown
} else { }
UnreadState.Unknown
}
}
for (i in (firstDisplayableEventIndex - 1) downTo 0) { for (i in (firstDisplayableEventIndex - 1) downTo 0) {
val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown
val eventId = timelineEvent.root.eventId ?: return UnreadState.Unknown val eventId = timelineEvent.root.eventId ?: return UnreadState.Unknown

View file

@ -33,8 +33,6 @@ class ScrollOnHighlightedEventCallback(private val recyclerView: RecyclerView,
private val scheduledEventId = AtomicReference<String?>() private val scheduledEventId = AtomicReference<String?>()
var timeline: Timeline? = null
override fun onInserted(position: Int, count: Int) { override fun onInserted(position: Int, count: Int) {
scrollIfNeeded() scrollIfNeeded()
} }
@ -45,9 +43,7 @@ class ScrollOnHighlightedEventCallback(private val recyclerView: RecyclerView,
private fun scrollIfNeeded() { private fun scrollIfNeeded() {
val eventId = scheduledEventId.get() ?: return val eventId = scheduledEventId.get() ?: return
val nonNullTimeline = timeline ?: return val positionToScroll = timelineEventController.searchPositionOfEvent(eventId)
val correctedEventId = nonNullTimeline.getFirstDisplayableEventId(eventId)
val positionToScroll = timelineEventController.searchPositionOfEvent(correctedEventId)
if (positionToScroll != null) { if (positionToScroll != null) {
val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition() val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition()
val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition() val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition()

View file

@ -17,48 +17,14 @@
package im.vector.app.features.home.room.detail.timeline.helper package im.vector.app.features.home.room.detail.timeline.helper
import im.vector.app.core.resources.UserPreferencesProvider 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 org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import javax.inject.Inject import javax.inject.Inject
class TimelineSettingsFactory @Inject constructor( class TimelineSettingsFactory @Inject constructor(private val userPreferencesProvider: UserPreferencesProvider) {
private val userPreferencesProvider: UserPreferencesProvider,
private val session: Session
) {
fun create(): TimelineSettings { fun create(): TimelineSettings {
return if (userPreferencesProvider.shouldShowHiddenEvents()) { return TimelineSettings(
TimelineSettings( initialSize = 30,
initialSize = 30, buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
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
)
}
} }
} }